Force.com Toolkit for Android の基本を学ぶ

摘要

Android は、Google が提供する、モバイルアプリケーション開発の主要なプラットフォームの 1 つです。一方 Force.com は、クラウドベースアプリケーション向けのプラットフォームとして業界トップクラスの地位を誇ります。Force.com Toolkit for Android は、Force.com と自社アプリケーションの強みを生かしたクラウドアプリケーションを Android 上で開発することを可能にするツールキットで、Android モバイルデバイスから Force.com データを参照、操作したいと考えている開発者を対象に設計されました。

前提条件

Force.com Toolkit for Android は、Android 2.2 プラットフォーム (API Level 8) 上で開発されています。記事内の説明は、Eclipse IDE のプラグインである ADT (Android Development Tools) を使用して Android アプリケーションの開発を行うこと、そして、Android SDK と Eclipse (ADT) のセットアップが完了していることを前提にしています。Android SDK とEclipse の ADT プラグインをダウンロードしてセットアップする手順については、Android SDK の Web サイト (英語) を参照してください。

また、この記事は、読者が Android アプリケーション開発の基本知識を持っていることを前提として、Force.com Toolkit for Android のセットアップ方法と Force.com データを参照、操作する方法に重点を置いて説明しています。Android アプリケーションの開発が初めてという方は、Android のサイトにあるチュートリアルを少なくとも 1 つは学習して、Android プラットフォームの基本概念とアーキテクチャについて把握しておくことを強くお勧めします。たとえば、Hello World tutorial (英語) などがわかりやすいでしょう。

ツールキットの使用を開始する

まず、Force.com Toolkit for Android を GitHub リポジトリ からダウンロードします。[Downloads] (ダウンロード) ボタンをクリックしてダウンロードすることも、任意の Git クライアントを使用してダウンロードすることも可能です。ツールキットの zip ファイルをローカルマシン上で解凍したら、Eclipse IDE で既存の Android プロジェクトへのインポートを行います。[Project] (プロジェクト)、[Properties] (プロパティ)、[Java Build Path] (Java のビルドパス)、[Source] (ソース)、[Link Source] (ソースのリンク) の順にクリックし、ツ―ルキットを解凍したフォルダのパス (例: C:\Temp\developerforce-Force.com-...\Sforce-Android-Toolkit) を指定して、フォルダ名 (例: 「Sforce-Android-Toolkit」) を入力すれば、インポートは完了です。これで、ツールキットを使い始めることができます。

Force.com Toolkit for Android は Force.com と HTTPS 経由で通信を行うため、Android アプリケーションには必ず「INTERNET」権限を含めます。この権限を有効にするには、アプリケーションの AndroidManifest.xml ファイルに、

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

というスクリプトを追加します。

メモ: Force.com Toolkit for Android には、使用方法を学習するためのサンプルアプリケーションも含まれています。まずこちらのサンプルアプリケーションを使ってみたいという場合は、新しい Android プロジェクトを作成し、[Create Project from existing source] (既存のソースからプロジェクトを作成) オプションを選択して、サンプルアプリケーションのルートディレクトリを指定します。その後、上に説明した手順 ([Link Source] (ソースのリンク) を使用する方法) に従って、ツールキットをサンプルアプリケーションにインポートする必要があります。

Force.com Toolkit for Android に関する基本事項

開発者がこのツールキットで扱う必要がある Java クラスは Salesforce.java のクラス 1 つのみで、これがファサード (Facade) となります。他のすべてのクラスで実装されたツールキットの機能は、この Facade クラスから利用できます。なお、Force.com Toolkit for Android は SOAP ベースの Web サービス API を使用して Force.com とデータをやりとりします。Salesforce.java には、現在 Force.com Toolkit for Android がサポートするすべての API コール (login、create、update など) に対応する静的メソッドが含まれているため、該当するメソッドを呼び出すだけで必要な操作を実行できます。

メモ: Force.com Toolkit for Android にアクセスするすべての Android アクティビティでは、他のメソッドを呼び出す前に Salesforce.init メソッドを呼び出す必要があります。これにより、ツールキットが適切に初期化され、アプリケーションで必要とされる状態が復元されます。

Force.com Toolkit for Android がサポートするコール

前に触れたように、Force.com Toolkit for Android では、Force.com の SOAP ベースの Web サービス API を基盤にして Force.com と通信します。そのため、このツールキットでサポートするメソッドは、それぞれの API コールに直接対応付けられます。現在サポートしている基本的な API コールは、以下の通りです。

  • login
  • logout
  • query
  • queryMore
  • queryAll
  • retrieve
  • create
  • update
  • upsert
  • delete

ほとんどの場合、メソッドの名前によって対応を確認できます。上記の各コールの詳細については、「Force.com SOAP API Developer's Guide」の「コアコール」の章を参照してください。

ログイン

Force.com Toolkit for Android では、ログインに関して loginloginOAuth の 2 つのメソッドをサポートしています。前者はユーザ名とパスワードにもとづく従来型の認証プロトコルを使用し、後者は OAuth 2.0 ユーザエージェントフローを使用します。Force.com Toolkit for Android のすべてのコンシューマでは、他のメソッドを呼び出す前に、この 2 つのメソッドのいずれかを呼び出す必要があります。その後実行されるすべてのコールでは、このログイン処理で返されたセッション ID とアクセストークンを使用して Force.com での認証を行います。

どちらのメソッドを使用するかは開発者が任意に選択できますが、モバイルアプリケーションの認証に関しては OAuth のほうが適しています。OAuth では、ユーザを Force.com にリダイレクトして認証と承認を実行するため、Android アプリケーション側でユーザ名とパスワードを取得して処理する必要がありません。さらに、login メソッドを使用する場合とは違って、loginOAuth メソッドでは Android アプリケーションでユーザ名とパスワードを取得するためのアクティビティを実装する必要がないというメリットもあります。こうした理由から、可能な限り loginOAuth メソッドの使用をお勧めします。

続いて、この 2 種類のログインメソッドの使用方法を説明します。

login

この後のコードスニペットは login メソッドの使用例を示しています。先ほど述べたように、このメソッドでは、通常、ユーザ入力によってユーザ名とパスワードを取得します。ユーザに対しては、EditText ウィジェットなどを使用して、Android でシンプルな入力用のフォームを表示します。なお、どのような場合にも、ユーザ名とパスワードを Android アプリケーションにハードコードすることは絶対に避けます。ユーザ名、パスワードのハードコーディングはコーディングのベストプラクティスに反しており、特に、セキュリティ保護されていないモバイルデバイスに展開されるアプリケーションでのハードコードは、非常に危険です。展開先の環境のセキュリティ設定によっては、メソッドにセキュリティトークンを渡すことが必要になる場合があります。

public class SforceLogin extends Activity {
    TextView username;
    TextView password;
    TextView securityToken;
    Button loginButton;
    Context context;

    /** アクティビティが最初に作成されたときに呼び出される */
    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.login_layout);
        setTitle("Sforce Toolkit Demo - Login");
        username = (TextView) this.findViewById(R.id.username);
        password = (TextView) this.findViewById(R.id.password);
        securityToken = (TextView) this.findViewById(R.id.securityToken);
        context = getApplicationContext();
                .....
        Salesforce.init(context);
    }

    public void loginOnClick(View v) {
        ConnectorConfig parameters =
                new ConnectorConfig(username.getText().toString(),
                password.getText().toString(),
                securityToken.getText().toString());
        try {
            Salesforce.login(parameters, new LoginResponseListener());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public class LoginResponseListener extends BaseResponseListener {
        @Override
        public void onComplete(Object sObjects) {
            LoginResult result = (LoginResult) sObjects;
            String id = result.getUserId();
            System.out.println("User id is:" + id);
            String orgId = result.getUserInfo().getOrganizationId();
            System.out.println("Org id is:" + orgId);
            Intent intent = new Intent();
            intent.setClass(context,
                    com.sforce.android.sample.SforceDisplayLoginResult.class);
            intent.putExtra("loginResult", result.toString());
            startActivity(intent);
        }

        @Override
        public void onSforceError(ApiFault apiFault) {
            String msg = apiFault.getExceptionMessage();
            System.out.println("Error msg:" + msg);
            String code = apiFault.getExceptionCode().getValue();
            System.out.println("Error code:" + code);
            if (code.equals(ExceptionCode._INVALID_LOGIN)) {
                System.out.println("Invalid login");
            }
        }
    }
}

サンドボックス環境に接続している場合は、ConnectorConfig クラスで setIsSandbox メソッドを呼び出す必要があります。なお、Force.com Toolkit for Android の Force.com Web サービス API のバージョンはデフォルトでは 20 になっていますが、setApiVersion メソッドを使用することで任意のバージョンにアップデートできます。

login コールによって返された結果 (LoginResult) を、ツールキットがサポートする他のメソッドに手動で渡す必要はありません (それらのメソッドがアプリケーション内の他の Android アクティビティによって呼び出されている場合も同様です)。後続のセクションで説明しますが、セッション情報の保持と共有は、ツールキットによって自動的に行われます。

loginOAuth

loginOAuth メソッドを呼び出す場合は、ターゲットの Force.com 環境で事前に OAuth 2.0 のセットアップを済ませておく必要があります。セットアップでは、[<あなたの名前>][設定][開発][リモートアクセス][新規] の順にクリックし、新しいリモートアクセスアプリケーションを登録します。

新しいリモートアクセスアプリケーションを登録する

アプリケーション名は自由に指定してかまいません。コールバック URL は、loginOAuth メソッドの呼び出しで使用される重要な URL であるため、有効な URI を指定するように注意してください。

コンシューマ鍵を取得する

新しいリモートアクセスアプリケーションを登録すると、コンシューマ鍵とコンシューマの秘密が生成されます (これは OAuth 2.0 プロトコルにもとづく処理になります)。コンシューマ鍵の値は後で loginOAuth メソッドに渡すことになるため、メモしておいてください。

OAuth のセットアップが完了したら、コンシューマ鍵とコールバック URL を渡して loginOAuth メソッドを呼び出すことが可能になります。このメソッドを呼び出すと、ユーザはモバイルデバイスでの表示用に最適化された Salesforce のログインページにリダイレクトされ、ログインとアプリケーションの承認を求められます。ユーザのログインと承認が完了すると、制御はアプリケーション側に戻され、登録されたリスナーへのコールバックによって loginOAuth メソッドが呼び出されます。

以下のコードスニペットは、loginOAuth メソッドの使用例を示しています。

public class SforceOAuthLogin extends Activity implements OnClickListener {
    String consumerKey = null;
    String callbackUrl = null;
    Button loginButton;
    Context context;

    /** アクティビティが最初に作成されたときに呼び出される */
    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.oauth_login_layout);
        context = getApplicationContext();
        ....
        //ここでは、Android アプリケーションに OAuth のコンシューマ鍵と
        //コールバック URL が String リソースとして格納されていることが前提
        consumerKey = (this.getResources().getString(R.string.consumerKey).toString());
        callbackUrl = (this.getResources().getString(R.string.callbackUrl).toString());
        Salesforce.init(context);
    }
    public void onClick(View v) {
        OAuthConnectorConfig parameters = new OAuthConnectorConfig(consumerKey, callbackUrl);
        try {
            Salesforce.loginOAuth(this, parameters, new LoginResponseListener());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public class LoginResponseListener extends BaseResponseListener {
        @Override
        public void onComplete(Object sObjects) {
            OAuthLoginResult result = (OAuthLoginResult) sObjects;
            String id = result.getUserId();
            setResult(RESULT_OK);
            finish();
        }

        @Override
        public void onSforceError(ApiFault apiFault) {
            String msg = apiFault.getExceptionMessage();
            String code = apiFault.getExceptionCode().getValue();
            if (code.equals(ExceptionCode._ACCESS_DENIED))
            {
                System.out.println("User didn't grant access");
            }
        }
    }
}

login コールのときと同様に、loginOAuth コールによって返された結果 (OAuthLoginResult) を、ツールキットがサポートする他のメソッドに渡す必要はありません。セッション情報の保持と共有は、Android アクティビティ間を通じて、ツールキットにより自動的に行われます。また、サンドボックス環境に接続している場合は、OAuthConnectorConfig クラスで setIsSandbox メソッドを呼び出す必要があります。

Android アクティビティ間におけるセッション情報の共有

アプリケーションの状態をどのように保持するか、すなわち、アプリケーションを構成するさまざまなアクティビティやプロセスの間でどのようにデータを共有するかは、あらゆる Android アプリケーションの開発において考慮すべき重要課題の 1 つです。

簡単な例として、Force.com のユーザ名とパスワードを取得してツールキットを起動するアクティビティを作成するケースを考えてみましょう。ツールキットへのログインが完了した後、別のアクティビティ (たとえば取引先レコードをクエリして表示するアクティビティ) でツールキットにアクセスするにはどうすればよいでしょうか。

Force.com Toolkit for Android では、標準の Android フレームワークである SharedPreferences を使用して、開発したアプリケーションの状態を管理するようになっています。login メソッドや loginAuth メソッドをいったん呼び出した後は、アクティビティ間でのセッション ID の受け渡しにわずらわされることはありません。こうした処理は、すべてツールキット内部で自動的に行われます。開発者側で必要な作業は、ツールキットへのアクセスを行うアクティビティごとに Salesforce.init メソッドを呼び出すことだけです。この init メソッドは、Salesforce.java の他のメソッドを実行する前に必ず呼び出す必要があり、通常はアクティビティの onCreate メソッド内に組み込みます。

Salesforce.init メソッドの使用例を以下に示します。

public class SforceUpdateAccount extends Activity {

    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.update_account_layout);
        Context context = getApplicationContext();
        Salesforce.init(context);
                .....
    }

    ....

    public void updateAccount()
    {
        .....
        Salesforce.update(sObjects, new UpdateResponseListener());
    }
}

レコードのクエリ

続いて、Force.com Toolkit for Android を使用して SOQL クエリを実行する方法を見てみましょう。

public class SforceQuery extends Activity implements OnClickListener {
    Context context;

    /** アクティビティが最初に作成されたときに呼び出される */
    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.query_layout);
        context = getApplicationContext();
        Salesforce.init(context);
                .....
    }

        .....

    public void queryAccount() {
        Salesforce.query("Select name, phone from Account", new QueryResponseListener());
    }

    public class QueryResponseListener extends BaseResponseListener {
        @Override
        public void onComplete(Object sObjects) {
            List<SObject> sObjectList = (ArrayList<SObject>) sObjects;
            StringBuffer collateRecords = new StringBuffer();
            for (SObject hm : sObjectList) {
                collateRecords.append(hm.toString());
                String acctName = hm.getField("Name");
            }

            final String display = collateRecords.toString();
            System.out.println(display);
        }

        @Override
        public void onSforceError(ApiFault apiFault) {
            String msg = apiFault.getExceptionMessage();
            String code = apiFault.getExceptionCode().getValue();
            if (code.equals(ExceptionCode._INVALID_FIELD))
            {
                System.out.println("Invalid field");
            }
        }
    }
}

コードで示されているように、クエリの手順は、Salesforce.java のクラスの query メソッドに SOQL クエリの文字列を渡すだけです。レスポンスは SObject の配列として返され、各 SObject をイントロスペクションすることにより、個々の項目を取得できます。

query コールで使用されているリスナーのデザインパターンに注意してください。同様のデザインは、ツールキット内のすべてのコールに適用されていますが、これにより、ツールキットに非同期の対話操作モデルが提供されています。各コールが内部で個別に Java スレッドを生成するため、Force.com からのレスポンスを待つ間、アプリケーションがブロックされることがなくなります。ツールキットが Force.com からのレスポンスを受け取ると、アプリケーションによって登録されたリスナーの onComplete メソッド (または onSforceError メソッド) が呼び出され、後続の処理が行われます。こうしたデザインパターンは、ネットワーク待機時間が深刻な問題となりうるアプリケーションの開発、特に Android のようなモバイルデバイス用のアプリケーションの開発では非常に重要になります。

query メソッドは、デフォルトで 500 件までのレコードを返すことができます。さらに多くのレコードをクエリしたい場合は、queryMore メソッドを使用します。ツールキットでは、queryMore パターンを使用した数千件規模のレコードのクエリがサポートされていますが、そのような大きなデータセットをモバイルデバイス上で処理することについては、慎重に検討する必要があります。多くの場合、数千件のレコードをモバイルデバイスでクエリして処理しようとすると、モバイルデバイスのシステムリソースでは対応できなくなります (数百件のクエリでも同様の状況が起こることがあります)。そのため、クエリは可能な限り細かく設定して、返されるデータセットのボリュームが抑えられるようにしてください。

メモ: Force.com Toolkit for Android の現在のバージョンでは、SOQL での子リレーションのクエリをサポートしていません。たとえば、以下のようなクエリは無効です。

select name, (select name from Contacts) from Account

新しいレコードの作成

続いて、新しいレコードを作成する処理を見てみましょう。次のコードスニペットでは、取引先レコードを新規に作成しています。

.....
public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    Context context = getApplicationContext();
    Salesforce.init(context);
            .....
}

 .....

public void createAccount()
{
    SObject obj = new SObject();
    //常に SObject のタイプを指定する必要がある
    obj.setType("Account");
    HashMap<String, String> requestFields = new HashMap<String, String>();
    requestFields.put("name", (name.getText().toString()).trim());
    requestFields.put("numberOfEmployees", (numberOfEmployees.getText().toString()).trim());

    obj.setFields(requestFields);
    ArrayList<SObject> objs = new ArrayList<SObject>();
    objs.add(obj);

    Salesforce.create(objs, new CreateResponseListener());
}

public class CreateResponseListener extends BaseResponseListener {
    @Override
    public void onComplete(final Object cresults) {
        ArrayList<SaveResult> resultArray = (ArrayList<SaveResult>) cresults;
        StringBuffer collateResults = new StringBuffer();
        for (SaveResult sr : resultArray) {
            if (sr.isSuccess()) {
                System.out.println("Success");
            } else {
                System.out.println("Record " + sr.getId() + " creation failed.");
                if (sr.getErrors() != null) {
                    System.out.println("Error Message: " +
                            sr.getErrors().get(0).getMessage());
                    System.out.println("Status Code: " +
                            sr.getErrors().get(0).getStatusCode().getValue());
                }
            }
        }
    }
    @Override
    public void onSforceError(ApiFault fault) {
        ......
    }
}

ご覧のように、この処理でも前と同様のリスナーのデザインパターンが使用されています。

レコードの更新

次のコードスニペットは、前のセクションで作成した取引先レコードを更新する処理を示しています。

.....
public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    Context context = getApplicationContext();
    Salesforce.init(context);
            .....
}

 .....

public void updateAccount()
{
    SObject obj = new SObject();
    //常に SObject のタイプを指定する必要がある
    obj.setType("Account");
    HashMap<String, String> requestFields = new HashMap<String, String>();
    requestFields.put("name", (name.getText().toString()).trim());
    //更新の実行時は Id 項目が必須
    requestFields.put("Id", (acctId.toString()).trim());

    obj.setFields(requestFields);

    //必要に応じて、特定の項目の値を明示的に null に設定することが可能
    ArrayList<String> fields2Null = new ArrayList<String>();
    fields2Null.add("Ownership");
    obj.setFieldsToNull(fields2Null);

    ArrayList<SObject> objs = new ArrayList<SObject>();
    objs.add(obj);

    Salesforce.update(objs, new UpdateResponseListener());
}

public class UpdateResponseListener extends BaseResponseListener {
    @Override
    public void onComplete(final Object cresults) {
        ArrayList<SaveResult> resultArray = (ArrayList<SaveResult>) cresults;
        StringBuffer collateResults = new StringBuffer();
        for (SaveResult sr : resultArray) {
            if (sr.isSuccess()) {
                System.out.println("Success");
            } else {
                System.out.println("Record " + sr.getId() + " update failed.");
                if (sr.getErrors() != null) {
                    System.out.println("Error Message: " +
                            sr.getErrors().get(0).getMessage());
                    System.out.println("Status Code: " +
                            sr.getErrors().get(0).getStatusCode().getValue());
                }
            }
        }
    }
    @Override
    public void onSforceError(ApiFault fault) {
        ......
    }
}

レコードの削除

以下のコードスニペットは、前のセクションで作成した取引先レコードを削除する処理を示しています。

    .....
public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
    Context context = getApplicationContext();
    Salesforce.init(context);
            .....
}

    .....

public void deleteAccount() {
    //id が Force.com ID のカンマ区切りリストであることが前提
    String deleteIds = ids.getText().toString();
    String[] ids = deleteIds.split(",");
    Salesforce.delete(ids, new DeleteResponseListener());
}

public class DeleteResponseListener extends BaseResponseListener {
    @Override
    public void onComplete(final Object cresults) {
        ArrayList<DeleteResult> resultArray = (ArrayList<DeleteResult>) cresults;
        StringBuffer collateResults = new StringBuffer();
        for (DeleteResult sr : resultArray) {
            if (sr.isSuccess()) {
                System.out.println("Success");
            } else {
                System.out.println("Record " + sr.getId() + " deletion failed.");
                if (sr.getErrors() != null) {
                    System.out.println("Error Message: " +
                            sr.getErrors().get(0).getMessage());
                    System.out.println("Status Code: " +
                            sr.getErrors().get(0).getStatusCode().getValue());
                }
            }
        }
    }
    @Override
    public void onSforceError(ApiFault fault) {
        ......
    }
}

まとめ

Force.com Toolkit for Android では、本来は複雑なプロセスから構成される Force.com との通信を容易に実行できます。このツールキットはデータのクエリ、更新、作成、削除を行うシンプルなインターフェイスを実装し、利便性の高い認証メカニズムである OAuth をサポートしています。一連の機能を活用することによって、Android 開発者は Force.com データを簡単に参照、操作できるようになります。

参考資料

執筆者について

Sandeep Bhanot は、セールスフォース・ドットコムのデベロッパーエバンジェリストです。