プログラマーのメモ書き

伊勢在住のプログラマーが気になることを気ままにメモったブログです

【Android】 Facebook連携アプリのサンプル(2/2):実装

前記事で、FacebookSDKを使うための準備ができましたので、いよいよコーディングです。

 

サンプルについて

今回作ったサンプルは次のような動作をするものとします。

  • アプリが起動すると自動的にFacebookにログインします
  • テキストを入力してボタンを押すと、本人のタイムラインに投稿します

ログアウトがないとか、その他問題がありそうな箇所はこの記事を参考にして、適当に補ってください。

 

ログイン機能について

AndroidでFacebookSDKを使ったログイン機能としては、大きく分けて、

  1. UserSettingsFragment クラスを利用したもの
  2. LoginButton ウィジェットを利用したもの
  3. UI部品を使わずに、自分でログイン・ログアウトの制御を行うもの

に分かれると思います(1.と2.は、FacebookSDKが提供するUIを使う形になります)。

それぞれの参考資料としては、

  1. FacebookSDK に含まれるSessionLoginSample サンプルプロジェクトのLoginUsingLoginFragment
  2. Facebookのチュートリアル(1 - Authenticate with Facebook Login)やHow To(Use Facebook Login
  3. Getting Started with the Facebook SDK for Android(簡単な例が載ってます)や、FacebookSDK に含まれるSessionLoginSample サンプルプロジェクトのLoginUsingActivityActivity

などがあります。

チュートリアルの充実ぶりなどからは、たぶん、LoginButton を使うのが推奨なんだろうけど、アプリへの組み込みを考えて3.のFacebookSDKのUI部品を使わずにログイン・ログアウト機能を実現してみました。

 

準備

Facebookにアプリを登録時に表示されたApp ID をAndroidManifest.xml に登録する必要があります。下記の2行をAndroidManifest.xml のapplication タグ内に追記します。

        <meta-data android:value="@string/app_id" android:name="com.facebook.sdk.ApplicationId"/>
        <activity android:name="com.facebook.LoginActivity"></activity>

なお、別途文字列として@string/app_id に実際のApp ID を定義しています。

また、インターネットへのアクセスを認めるパーミッションも追記しておきます。

    <uses-permission android:name="android.permission.INTERNET"/>

 

ログイン用のレイアウトは、テキストビューとボタンをひとつだけ持ったものとします。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <Button
        android:id="@+id/post_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:text="投稿" />

    <EditText
        android:id="@+id/message"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_above="@+id/post_button"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:ems="10"
        android:gravity="top|left"
        android:inputType="textMultiLine" />

</RelativeLayout>

 

Activityの実装

Facebookではログイン・ログアウトの制御はSessionが行います。また、ログイン状態が変更した場合、Session.StatusCallback 経由で通知を受け取ることができます。

全体の流れは、FacebookSDK に含まれるSessionLoginSample サンプルプロジェクトのLoginUsingActivityActivityを元にしています。

 

あと、Facebookではログイン時に多くのパーミッションを与えるのではなく、ログイン時は最低限必要なパーミッションのみを与えて、アプリを使うなかで必要なパーミッションが出てくれば、再度ユーザーに確認をとる方式を推奨しています。

そして、投稿には、Extended Permissionspublish_actions という書き込み用のパーミッションが必要になります。

このため、投稿ボタン押下時には、現在持っているパーミッションを確認して、不足しているなら再度ユーザーに確認を求めるという処理が必要になります。

 

したがってFacebookにログインしていない状態であれば、投稿までには2回確認ダイアログが表示されます。なお、Google Play からFacebookアプリをダウンロードしてインストールしている場合とインストールしていない場合で挙動が若干異なります。

 

これらを考慮して作ったものが、下記のものになります。

package com.example.facebooksample;

import java.util.Arrays;
import java.util.Date;
import java.util.List;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import com.facebook.FacebookAuthorizationException;
import com.facebook.FacebookOperationCanceledException;
import com.facebook.FacebookRequestError;
import com.facebook.Request;
import com.facebook.Response;
import com.facebook.Session;
import com.facebook.Session.OpenRequest;
import com.facebook.SessionState;
import com.facebook.model.GraphObject;

public class MainActivity extends Activity {

    private static final String TAG = "MainActivity";
    private static final List<String> PERMISSIONS = Arrays.asList("publish_actions");
    private EditText mEditText;
    private class SessionStatusCallback implements Session.StatusCallback {
        @Override
        public void call(Session session, SessionState state, Exception exception) {
            //updateView();
            Log.d(TAG, "SessionStatusCallback");
            onSessionStateChange(session, state, exception);
        }
    }
    private Session.StatusCallback statusCallback = new SessionStatusCallback();
    
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d(TAG,"onCreate");
        
        // 投稿用UI設定
        mEditText = (EditText) findViewById(R.id.message);
        Button btn = (Button) findViewById(R.id.post_button);
        btn.setOnClickListener(new OnClickListener() {
            
            @Override
            public void onClick(View v) {
                //doPost();
                preparePublish();
            }
        });
        
        // Facebook ログイン管理
        Session session = Session.getActiveSession();
        if (session == null) {
            if (savedInstanceState != null) {
                session = Session.restoreSession(this, null, statusCallback, savedInstanceState);
            }
            if (session == null) {
                session = new Session(this);
            }
            Session.setActiveSession(session);
            if (session.getState().equals(SessionState.CREATED_TOKEN_LOADED)) {
                //session.openForPublish(getOpenRequest());
                session.openForRead(new OpenRequest(this));
            }
        }

        // ログイン状態の確認
        if (! session.isOpened()) {
            doLogin();
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }
    @Override
    public void onStart() {
        super.onStart();
        Session.getActiveSession().addCallback(statusCallback);
        Log.d(TAG,"onStart");
    }

    @Override
    public void onStop() {
        super.onStop();
        Session.getActiveSession().removeCallback(statusCallback);
        Log.d(TAG,"onStop");
    }
    @Override
    protected void onResume() {
        super.onResume();
        Session session = Session.getActiveSession();
        Log.d(TAG,"onResume:" + "session state is " + session.getState());

        if (session.getState().equals(SessionState.CLOSED_LOGIN_FAILED) || session.getState().equals(SessionState.CLOSED)) {
            Toast.makeText(this, "Facebook認証に失敗しました。", Toast.LENGTH_LONG).show();
            finish();
        }
    }
    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG,"onPause");
    }
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        Log.d(TAG,"onActivityResult");
        Session.getActiveSession().onActivityResult(this, requestCode, resultCode, data);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Log.d(TAG,"onSaveInstanceState");
        Session session = Session.getActiveSession();
        Session.saveSession(session, outState);
    }

    private void onSessionStateChange(Session session, SessionState state, Exception exception) {
        if ((exception instanceof FacebookOperationCanceledException ||
                exception instanceof FacebookAuthorizationException)) {
            Log.w(TAG, "error occured:" + exception.getMessage());
        } else if (state == SessionState.OPENED_TOKEN_UPDATED) {
            doPost();
        }
    }
    
    private void doLogin() {
        Session session = Session.getActiveSession();
        Log.d(TAG,"doLogin: session state is " + session.getState() + ", isOpend:" + session.isOpened() + ", isClosed:" + session.isClosed());
        if (!session.isOpened()) {
            if (session.isClosed()) {
                session = new Session(this);
                Session.setActiveSession(session);          
            }
            //session.openForPublish(getOpenRequest());
            session.openForRead(new OpenRequest(this));
        } else {
            Session.openActiveSession(this, true, statusCallback);
        }
    }

    private boolean hasPublishPermission() {
        Session session = Session.getActiveSession();
        return session != null && session.getPermissions().contains("publish_actions");
    }

    private void preparePublish() {
        Session session = Session.getActiveSession();
        if (session != null) {
            if (hasPublishPermission()) {
                // We can do the action right away.
                doPost();
            } else {
                // We need to get new permissions, then complete the action when we get called back.
                session.requestNewPublishPermissions(new Session.NewPermissionsRequest(this, PERMISSIONS));
            }
        }
    }
    
    private void doPost() {
        Log.d(TAG,"doPost");
        final String message = "投稿テストです:" + (new Date().toString()) + "n" + mEditText.getText().toString();
        Request request = Request
                .newStatusUpdateRequest(Session.getActiveSession(), message, new Request.Callback() {
                    @Override
                    public void onCompleted(Response response) {
                        showPublishResult(message, response.getGraphObject(), response.getError());
                    }
                });
        request.executeAsync();     
    }

    private interface GraphObjectWithId extends GraphObject {
        String getId();
    }
    private void showPublishResult(String message, GraphObject result, FacebookRequestError error) {
        Log.d(TAG,"showPublishResult");
        String title = null;
        String alertMessage = null;
        if (error == null) {
            title = "ステータスアップデート成功";
            String id = result.cast(GraphObjectWithId.class).getId();
            alertMessage = "ID=" + id;
        } else {
            title = "ステータスアップデート失敗";
            alertMessage = error.getErrorMessage();
        }

        new AlertDialog.Builder(this)
                .setTitle(title)
                .setMessage(alertMessage)
                .setPositiveButton("OK", null)
                .show();
    }
    
}

投稿は、Request#newStatusUpdateRequestメソッドを利用して行っています。

 

ここまでできたら、アプリを実行すると、Facebookへのログインが確認され、問題なく投稿できることがわかります。

 

(参考)

  • パーミッションの追加処理については、たとえば、Publish to Feed Step2あたりを参考にしてください
  • パーミッションそのものについては、Permissions などを参考にしてください

 

おまけ:タイムラインへ画像つきで投稿

画像つきで投稿する場合は、Request#newUploadPhotoRequest メソッドを使えばできます。

しかし、メソッドの引数を見ると、テキストメッセージを与える部分がありません。調べてみると、これは、requestオブジェクトのパラメータを使う必要があるそうです。

たとえば、このようにします(画像は、mImageに入っているとします)。

 

    final String message = mEditText.getText().toString();
    Request request = Request.newUploadPhotoRequest(Session.getActiveSession(), mImage,
            new Request.Callback() {
                @Override
                public void onCompleted(Response response) {
                    // 必要があれば投稿完了時の処理
                    FacebookPostActivity.this.finish();
                }
            });
    Bundle params = request.getParameters();
    params.putString("message", message);
    request.executeAsync();

 

これで、メッセージ+画像で投稿できます。

 

(参考)

Post pic on wall with message with Android Facebook SDK 3.0 - Stack Overflow