メインコンテンツへスキップ
このドキュメントは Mobile + API アーキテクチャシナリオの一部であり、Android でモバイルアプリケーションを実装する方法について説明します。実装するソリューションの詳細については、このシナリオを参照してください。

ステップ 1. アプリケーションをセットアップする

サンプルプロジェクト

まずは、このチュートリアル用のサンプルプロジェクトをダウンロードしてください。 ダウンロード GitHub でフォーク システム要件
  • Android Studio 2.3
  • Android SDK 25
  • エミュレーター - Nexus 5X - Android 6.0
要件を表示

依存関係を設定する

この実装では、アプリのbuild.gradleファイルで次の依存関係を使用します。
  • Auth0.Android: このパッケージを使用すると、Auth0 と統合してユーザーを認証できます。
  • OkHttp: このパッケージは、Node.js API にリクエストを送信するための HTTP クライアントを提供します。
  • JWTDecode.Android: このパッケージは、のデコードに役立ちます。
  • AppCompat: このパッケージを使用すると、アクティビティ内のナビゲーションに toolbar ウィジェットを利用できます。
dependencies {
    compile 'com.squareup.okhttp:okhttp:2.7.5'
    compile 'com.auth0.android:auth0:1.10.0'
    compile 'com.auth0.android:jwtdecode:1.1.1'
    compile 'com.android.support:appcompat-v7:25.3.1'
    testCompile 'junit:junit:4.12'
}

マニフェストを更新する

アプリケーションのAndroidManifest.xmlを開き、インターネット アクセス権限を追加します。
<uses-permission android:name="android.permission.INTERNET" />
アプリケーションの詳細も更新し、Toolbar ウィジェットを利用するようにします。
<application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AppCompat.Light.NoActionBar">
    </application>

設定値を指定する

/res/values/strings.xml にある strings.xml リソースに、Auth0 の 、Auth0 ドメイン、および API の URL を設定します。
<resources>
    <string name="app_name">ExampleCo Timesheets</string>
    <string name="login">Log in</string>
    <string name="auth0_client_id">...</string>
    <string name="auth0_domain">...</string>
    <string name="api_url">http://10.0.2.2:8080/timesheets</string>
</resources>
この実装では、アプリケーションパッケージ内に activitiesmodelsutils 用のディレクトリを作成します。
  • activities/: このパッケージには、LoginActivity.javaTimeSheetActivity.javaFormActivity.javaUserActivity.java を配置します。
  • models/: このパッケージには、TimeSheet.javaUser.java のデータモデルを配置します。
  • utils/: このパッケージには、UserProfileManager.javaTimeSheetAdapter.javaImageTask.java を配置します。

ステップ 2. ユーザーを認可する

マニフェストを更新する

アプリのAndroidManifest.xmlを開き、LoginActivityを追加します。
<activity
            android:name="com.auth0.samples.activities.LoginActivity"
            android:launchMode="singleTask">

            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>

            <intent-filter>
                <action android:name="android.intent.action.VIEW" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data
                    android:host="@string/auth0_domain"
                    android:pathPrefix="/android/com.auth0.samples/callback"
                    android:scheme="demo" />
            </intent-filter>
</activity>

Login Activity を作成する

LoginActivity はユーザーの認可を処理し、ユーザーに最初に表示される画面になります。WebAuthProvider を初期化して認証を開始するための login() メソッドを作成します。WebAuthProvider には、正しい scheme、、およびスコープを指定してください。この実装では次を使用します。
  • scheme: demo
  • audience: https://api.exampleco.com/timesheets (Node.JS API)
  • response_type: code
  • scope: create:timesheets read:timesheets openid profile email offline_access。これらのスコープにより、Node.JS API に対して POSTGET を実行できるほか、ユーザープロフィールと を取得できます。
private void login() {
        Auth0 auth0 = new Auth0(getString(R.string.auth0_client_id), getString(R.string.auth0_domain));
        auth0.setOIDCConformant(true);

        WebAuthProvider.init(auth0)
                .withScheme("demo")
                .withAudience("https://api.exampleco.com/timesheets")
                .withResponseType(ResponseType.CODE)
                .withScope("create:timesheets read:timesheets openid profile email offline_access")
                .start(
                    // ...
                );
}
login() メソッドでは、認証に成功すると、ユーザーは TimeSheetActivity にリダイレクトされます。
package com.auth0.samples.activities;

import android.app.Activity;
import android.app.Dialog;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import com.auth0.android.Auth0;
import com.auth0.android.authentication.AuthenticationException;
import com.auth0.android.jwt.JWT;
import com.auth0.android.provider.AuthCallback;
import com.auth0.android.provider.ResponseType;
import com.auth0.android.provider.WebAuthProvider;
import com.auth0.android.result.Credentials;
import com.auth0.samples.R;
import com.auth0.samples.models.User;
import com.auth0.samples.utils.CredentialsManager;
import com.auth0.samples.utils.UserProfileManager;

public class LoginActivity extends Activity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.login_activity);
        Button loginWithTokenButton = (Button) findViewById(R.id.loginButton);
        loginWithTokenButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                login();
            }
        });
    }

    @Override
    protected void onNewIntent(Intent intent) {
        if (WebAuthProvider.resume(intent)) {
            return;
        }
        super.onNewIntent(intent);
    }

    private void login() {
        Auth0 auth0 = new Auth0(getString(R.string.auth0_client_id), getString(R.string.auth0_domain));
        auth0.setOIDCConformant(true);

        WebAuthProvider.init(auth0)
                .withScheme("demo")
                .withAudience("https://api.exampleco.com/timesheets")
                .withResponseType(ResponseType.CODE)
                .withScope("create:timesheets read:timesheets openid profile email")
                .start(LoginActivity.this, new AuthCallback() {
                    @Override
                    public void onFailure(@NonNull final Dialog dialog) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                dialog.show();
                            }
                        });
                    }

                    @Override
                    public void onFailure(final AuthenticationException exception) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                Toast.makeText(LoginActivity.this, "Error: " + exception.getMessage(), Toast.LENGTH_SHORT).show();
                            }
                        });
                    }

                    @Override
                    public void onSuccess(@NonNull final Credentials credentials) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                Toast.makeText(LoginActivity.this, "Log In - Success", Toast.LENGTH_SHORT).show();
                            }
                        });
                        startActivity(new Intent(LoginActivity.this, TimeSheetActivity.class));
                    }
                });
    }
}

認証情報を保存する

ログイン後に受け取った認証情報を保存するには、Auth0.Android ライブラリの CredentialsManager と、保存先として SharedPreferences を使用します。 login() メソッドで WebAuthProvider を初期化する前に、CredentialsManager を作成できます。AuthenticationAPIClientCredentialsManager に渡すと、有効期限が切れた場合に を更新できるようになります。
private void login() {
        Auth0 auth0 = new Auth0(getString(R.string.auth0_client_id), getString(R.string.auth0_domain));
        auth0.setOIDCConformant(true);

        AuthenticationAPIClient authAPIClient = new AuthenticationAPIClient(auth0);
        SharedPreferencesStorage sharedPrefStorage = new SharedPreferencesStorage(this);
        final CredentialsManager credentialsManager = new CredentialsManager(authAPIClient, sharedPrefStorage);

        WebAuthProvider.init(auth0)
        // ...
    }
認証が成功したら認証情報がCredentialsManagerを介して保存されるように、login()メソッドを更新します。
// ...
@Override
public void onSuccess(@NonNull final Credentials credentials) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            Toast.makeText(LoginActivity.this, "Log In - Success", Toast.LENGTH_SHORT).show();
        }
    });

    credentialsManager.saveCredentials(credentials);
    startActivity(new Intent(LoginActivity.this, TimeSheetActivity.class));
// ...
}

ステップ 3. ユーザープロファイルを取得する

ユーザーモデルを作成する

UserProfileManagerUserActivity で使用する、シンプルなユーザーモデルを作成します。
package com.auth0.samples.models;

public class User {
    private String email;
    private String name;
    private String pictureURL;

    public User(String email, String name, String pictureURL) {
        this.email = email;
        this.name = name;
        this.pictureURL = pictureURL;
    }

    public String getEmail() {
        return email;
    }

    public String getName() {
        return name;
    }

    public String getPictureURL() {
        return pictureURL;
    }
}

ユーザープロファイルを保存する

ユーザープロファイル情報を保存するために、UserProfileManager マネージャークラスを作成します。UserProfileManager は、データの保存に SharedPreferences を使用します。
package com.auth0.samples.utils;

import android.content.Context;
import android.content.SharedPreferences;

import com.auth0.android.result.UserProfile;
import com.auth0.samples.models.User;

public class UserProfileManager {

    private static final String PREFERENCES_NAME = "auth0_user_profile";
    private static final String EMAIL = "email";
    private static final String NAME = "name";
    private static final String PICTURE_URL = "picture_url";

    public static void saveUserInfo(Context context, User userInfo) {
        SharedPreferences sp = context.getSharedPreferences(
                PREFERENCES_NAME, Context.MODE_PRIVATE);

        sp.edit()
                .putString(EMAIL, userInfo.getEmail())
                .putString(NAME, userInfo.getName())
                .putString(PICTURE_URL, userInfo.getPictureURL())
                .apply();
    }

    public static User getUserInfo(Context context) {
        SharedPreferences sp = context.getSharedPreferences(
                PREFERENCES_NAME, Context.MODE_PRIVATE);

        return new User(
                sp.getString(EMAIL, null),
                sp.getString(NAME, null),
                sp.getString(PICTURE_URL, null)
        );
    }

    public static void deleteUserInfo(Context context) {
        SharedPreferences sp = context.getSharedPreferences(
                PREFERENCES_NAME, Context.MODE_PRIVATE);

        sp.edit()
                .putString(EMAIL, null)
                .putString(NAME, null)
                .putString(PICTURE_URL, null)
                .apply();
    }
}
次に、LoginActivitylogin() メソッドを更新して、 を取得し、JWTDecode.Android ライブラリを使用してトークンからユーザープロファイルを取り出します。続いて、そのユーザープロファイルを UserProfileManager に保存します。
// ...
@Override
public void onSuccess(@NonNull final Credentials credentials) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            Toast.makeText(LoginActivity.this, "Log In - Success", Toast.LENGTH_SHORT).show();
        }
    });

    credentialsManager.saveCredentials(credentials);
    JWT jwt = new JWT(credentials.getIdToken());
    User user = new User(
            jwt.getClaim("email").asString(),
            jwt.getClaim("name").asString(),
            jwt.getClaim("picture").asString()
    );
    UserProfileManager.saveUserInfo(LoginActivity.this, user);

    startActivity(new Intent(LoginActivity.this, TimeSheetActivity.class));
}
// ...

ステップ 4. スコープに基づいて UI 要素を条件付きで表示する

ユーザーに特定の操作を実行する権限があるかどうかを判断するには、認証プロセス中にそのユーザーに付与された scope を確認します。scope にはユーザーに付与されたすべてのスコープを含む文字列が格納されるため、特定のスコープが付与されているかどうかを確認するには、スコープ文字列にそのスコープに対応する部分文字列が含まれているかを確認するだけです。

スコープを保存する

まず、付与されたスコープを保存できるように User クラスを更新します。次に、付与されたスコープに特定のスコープが含まれているかどうかを判定するためのヘルパーメソッド hasScope() を追加します。
public class User {
    private String email;
    private String name;
    private String pictureURL;
    private String grantedScope;

    public User(String email, String name, String pictureURL, String grantedScope) {
        this.email = email;
        this.name = name;
        this.pictureURL = pictureURL;
        this.grantedScope = grantedScope;
    }

    public String getEmail() {
        return email;
    }

    public String getGrantedScope() { 
        return grantedScope; 
    }

    public String getName() {
        return name;
    }

    public String getPictureURL() {
        return pictureURL;
    }

    public Boolean hasScope(String scope) {
        return grantedScope.contains(scope);
    }
}
追加のフィールドを保存できるよう、UserProfileManager も忘れずに更新してください。
public class UserProfileManager {

    private static final String PREFERENCES_NAME = "auth0_user_profile";
    private static final String EMAIL = "email";
    private static final String NAME = "name";
    private static final String PICTURE_URL = "picture_url";
    private static final String SCOPE = "scope";

    public static void saveUserInfo(Context context, User userInfo) {
        SharedPreferences sp = context.getSharedPreferences(
                PREFERENCES_NAME, Context.MODE_PRIVATE);

        sp.edit()
                .putString(EMAIL, userInfo.getEmail())
                .putString(NAME, userInfo.getName())
                .putString(PICTURE_URL, userInfo.getPictureURL())
                .putString(SCOPE, userInfo.getGrantedScope())
                .apply();
    }

    public static User getUserInfo(Context context) {
        SharedPreferences sp = context.getSharedPreferences(
                PREFERENCES_NAME, Context.MODE_PRIVATE);

        return new User(
                sp.getString(EMAIL, null),
                sp.getString(NAME, null),
                sp.getString(PICTURE_URL, null),
                sp.getString(SCOPE, null)
        );
    }

    public static void deleteUserInfo(Context context) {
        SharedPreferences sp = context.getSharedPreferences(
                PREFERENCES_NAME, Context.MODE_PRIVATE);

        sp.edit()
                .putString(EMAIL, null)
                .putString(NAME, null)
                .putString(PICTURE_URL, null)
                .putString(SCOPE, null)
                .apply();
    }
}
次に、LoginActivity を更新し、scope を渡して User オブジェクトに保存できるようにします。
// ...
@Override
public void onSuccess(@NonNull final Credentials credentials) {
    runOnUiThread(new Runnable() {
        @Override
        public void run() {
            Toast.makeText(LoginActivity.this, "Log In - Success", Toast.LENGTH_SHORT).show();
        }
    });

    credentialsManager.saveCredentials(credentials);
    JWT jwt = new JWT(credentials.getIdToken());
    String scopes = credentials.getScope();
    User user = new User(
            jwt.getClaim("email").asString(),
            jwt.getClaim("name").asString(),
            jwt.getClaim("picture").asString(),
            credentials.getScope()
    );
    UserProfileManager.saveUserInfo(LoginActivity.this, user);

    startActivity(new Intent(LoginActivity.this, TimeSheetActivity.class));
}
// ...

スコープに基づいて承認メニューを表示する

これで、ユーザーに特定のスコープが付与されているかどうかに応じて、特定の UI 要素を表示できるようになります。たとえば、承認メニュー項目は、approve:timesheets スコープが付与されているユーザーにのみ表示されるようにする必要があります。 以下は、ユーザーが approve:timesheets スコープを持っているかどうかを BaseActivity クラスで確認し、その結果に応じて承認アクティビティを表示するメニュー項目の表示/非表示を設定するコードです。
// ...
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    Boolean canApprove = UserProfileManager.getUserInfo(this).hasScope("approve:timesheets");
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.actions, menu);
    MenuItem item = menu.findItem(R.id.action_approve);
    item.setVisible(canApprove);
    return super.onCreateOptionsMenu(menu);
}
// ...

手順 5. API を呼び出す

マニフェスト を更新する

アプリの AndroidManifest.xml を開き、TimeSheetActivity を追加します:
<activity android:name="com.auth0.samples.activities.TimeSheetActivity" />

Timesheets Activity のレイアウトを作成する

次に、TimeSheetsActivity 用のレイアウト timesheet_activity.xml を作成します。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.Toolbar
        android:id="@+id/navToolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:elevation="4dp"
        android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

    <ListView
        android:id="@+id/timesheetList"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>
ListViewウィジェットには、item_entry.xmlレイアウトで表される各エントリが含まれます。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tvUserID"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="UserID"
        android:textStyle="bold" />
    <TextView
        android:id="@+id/tvDate"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Date" />

    <TextView
        android:id="@+id/tvProjectName"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Project"
        android:textStyle="italic" />
    <TextView
        android:id="@+id/tvHours"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hours" />
</LinearLayout>
また、TimeSheetActivity の Toolbar のナビゲーション用に、timesheet_action_menu.xml のメニューリソース (/res/menu/) を作成します。
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/action_profile"
        android:title="Profile"
        app:showAsAction="always" />
    <item
        android:id="@+id/action_new"
        android:title="New Timesheet"
        app:showAsAction="always" />
</menu>

タイムシートモデルを作成する

ビューでタイムシートデータを扱うためのモデルを作成します。
package com.auth0.samples.models;

import java.util.Date;

/**
 * ej により 7/9/17 に作成。
 */

public class TimeSheet {
    private String userID;
    private String projectName;
    private String date;
    private double hours;
    private int ID;

    public TimeSheet(String gUserID, String gProjectName, String gDate, double gHours, int gID) {
        this.userID = gUserID;
        this.projectName = gProjectName;
        this.date = gDate;
        this.hours = gHours;
        this.ID = gID;
    }

    public String getUserID() {
        return userID;
    }

    public String getProjectName() {
        return projectName;
    }

    public String getDateString() {
        return date;
    }

    public double getHours() {
        return hours;
    }

    public int getID() {
        return ID;
    }
}

TimeSheetAdapter を作成する

TimeSheetAdapterは、タイムシートエントリの配列を受け取り、TimeSheetActivityListViewに反映するユーティリティクラスです。
package com.auth0.samples.utils;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import com.auth0.samples.R;
import com.auth0.samples.models.TimeSheet;
import java.util.ArrayList;

public class TimeSheetAdapter extends ArrayAdapter<TimeSheet> {

    public TimeSheetAdapter(Context context, ArrayList<TimeSheet> timesheets) {
        super(context, 0, timesheets);
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {

        TimeSheet timesheet = getItem(position);

        if (convertView == null) {
            convertView = LayoutInflater.from(getContext()).inflate(R.layout.item_entry, parent, false);
        }

        TextView tvUserID = (TextView) convertView.findViewById(R.id.tvUserID);
        TextView tvDate = (TextView) convertView.findViewById(R.id.tvDate);
        TextView tvProjectName = (TextView) convertView.findViewById(R.id.tvProjectName);
        TextView tvHours = (TextView) convertView.findViewById(R.id.tvHours);

        tvUserID.setText(timesheet.getUserID());
        tvDate.setText(timesheet.getDateString());
        tvProjectName.setText(timesheet.getProjectName());
        tvHours.setText(Double.toString(timesheet.getHours()));

        return convertView;
    }
}

Timesheet Activity を作成する

TimeSheetActivity は、サーバーに保存されている、ログイン中のユーザーのタイムシートエントリを表示します。
  • Android Emulator から http://localhost:8080 で実行中の Node.JS API に接続できるよう、@string/api_urlhttp://10.0.2.2:8080/timesheets に設定されています。
  • callAPI() メソッドは、Node.JS API からタイムシートを取得します。
  • processResults() メソッドは、callAPI() から返された JSON レスポンスを TimeSheet オブジェクトに変換します。
  • onCreateOptionsMenu() メソッドと onOptionsItemSelected() メソッドは、Toolbar ウィジェットのナビゲーション機能を処理します。
package com.auth0.samples.activities;

import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.ListView;
import android.widget.Toast;

import com.auth0.android.Auth0;
import com.auth0.android.authentication.AuthenticationAPIClient;
import com.auth0.android.authentication.storage.CredentialsManager;
import com.auth0.android.authentication.storage.CredentialsManagerException;
import com.auth0.android.authentication.storage.SharedPreferencesStorage;
import com.auth0.android.callback.BaseCallback;
import com.auth0.android.result.Credentials;
import com.auth0.samples.R;
import com.auth0.samples.utils.TimeSheetAdapter;
import com.auth0.samples.models.TimeSheet;
import com.squareup.okhttp.Callback;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.util.ArrayList;

public class TimeSheetActivity extends AppCompatActivity {

    private ArrayList<TimeSheet> timesheets = new ArrayList<>();

    private String accessToken;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.timesheet_activity);
        Toolbar navToolbar = (Toolbar) findViewById(R.id.navToolbar);
        setSupportActionBar(navToolbar);

        Auth0 auth0 = new Auth0(getString(R.string.auth0_client_id), getString(R.string.auth0_domain));
        auth0.setOIDCConformant(true);

        AuthenticationAPIClient authAPIClient = new AuthenticationAPIClient(auth0);
        SharedPreferencesStorage sharedPrefStorage = new SharedPreferencesStorage(this);

        CredentialsManager credentialsManager = new CredentialsManager(authAPIClient, sharedPrefStorage);
        credentialsManager.getCredentials(new BaseCallback<Credentials, CredentialsManagerException>() {
            @Override
            public void onSuccess(Credentials payload) {
                accessToken = payload.getAccessToken();
                callAPI();
            }

            @Override
            public void onFailure(CredentialsManagerException error) {
                Toast.makeText(TimeSheetActivity.this, "Error: " + error.getMessage(), Toast.LENGTH_SHORT).show();
            }
        });
    }

    private void callAPI() {
        final Request.Builder reqBuilder = new Request.Builder()
                .get()
                .url(getString(R.string.api_url))
                .addHeader("Authorization", "Bearer " + accessToken);

        OkHttpClient client = new OkHttpClient();
        Request request = reqBuilder.build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Request request, IOException e) {
                Log.e("API", "Error: ", e);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(TimeSheetActivity.this, "An error occurred", Toast.LENGTH_SHORT).show();
                    }
                });
            }

            @Override
            public void onResponse(final Response response) throws IOException {
                timesheets = processResults(response);
                final TimeSheetAdapter adapter = new TimeSheetAdapter(TimeSheetActivity.this, timesheets);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (response.isSuccessful()) {
                            ListView listView = (ListView) findViewById(R.id.timesheetList);
                            listView.setAdapter(adapter);
                            adapter.addAll(timesheets);
                        } else {
                            Toast.makeText(TimeSheetActivity.this, "API call failed.", Toast.LENGTH_SHORT).show();
                        }
                    }
                });
            }
        });
    }

    private ArrayList<TimeSheet> processResults (Response response) {
        ArrayList<TimeSheet> timesheets = new ArrayList<>();
        try {
            String jsonData = response.body().string();
            if (response.isSuccessful()) {
                JSONArray timesheetJSONArray = new JSONArray(jsonData);
                for (int i = 0; i < timesheetJSONArray.length(); i++) {
                    JSONObject timesheetJSON = timesheetJSONArray.getJSONObject(i);
                    String userID = timesheetJSON.getString("user_id");
                    String projectName = timesheetJSON.getString("project");
                    String dateStr = timesheetJSON.getString("date");
                    Double hours = timesheetJSON.getDouble("hours");
                    int id = timesheetJSON.getInt("id");

                    TimeSheet timesheet = new TimeSheet(userID, projectName, dateStr, hours, id);
                    timesheets.add(timesheet);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (JSONException e) {
            e.printStackTrace();
        }
        return timesheets;
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.timesheet_action_menu, menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_new:
                startActivity(new Intent(TimeSheetActivity.this, FormActivity.class));
                break;
            case R.id.action_profile:
                startActivity(new Intent(TimeSheetActivity.this, UserActivity.class));
                break;
            default:
                return super.onOptionsItemSelected(item);
        }
        return true;
    }
}

ステップ 6. ユーザープロファイルを表示する

ログイン中のユーザーのプロフィールを表示するため、UserActivity、対応する user_activity.xml レイアウト、そして Toolbar ナビゲーション用の user_action_menu.xml を作成します。このビューには、ユーザーの名前、メールアドレス、プロフィール画像が表示されます。

マニフェストを更新する

アプリの AndroidManifest.xml を開き、UserActivity を追加します。
<activity android:name="com.auth0.samples.activities.UserActivity" />

ユーザーアクティビティのレイアウトを作成する

次に、プロフィール画像用の ImageView と、ユーザーの名前およびメールアドレス用の TextViews を含む、UserActivity のレイアウト user_activity.xml を作成します。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v7.widget.Toolbar
        android:id="@+id/navToolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:elevation="4dp"
        android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

    <ImageView
        android:id="@+id/ivPicture"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:srcCompat="@android:color/darker_gray" />

    <TextView
        android:id="@+id/tvName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Name"
        android:textSize="24sp" />

    <TextView
        android:id="@+id/tvEmail"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Email"
        android:textSize="24sp" />
</LinearLayout>
UserActivity の Toolbar 用に user_actions_menu.xml を作成します。
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/action_view"
        android:title="View Timesheets"
        app:showAsAction="always" />
    <item
        android:id="@+id/action_new"
        android:title="New Timesheet"
        app:showAsAction="always" />
</menu>

URLからプロフィール画像を読み込む

URLからユーザーのプロフィール画像を読み込むには、AsyncTask を継承し、バックグラウンドで実行するタスクを作成します。
package com.auth0.samples.utils;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.util.Log;
import android.widget.ImageView;

import java.io.InputStream;
import java.net.URL;

public class ImageTask extends AsyncTask<String, Void, Bitmap> {

    private ImageView bmImage;

    public ImageTask(ImageView bmImage) {
        this.bmImage = bmImage;
    }

    protected Bitmap doInBackground(String... urls) {
        String urldisplay = urls[0];
        Bitmap mIcon11 = null;

        try {
            InputStream in = new URL(urldisplay).openStream();
            mIcon11 = BitmapFactory.decodeStream(in);
        } catch (Exception e) {
            Log.e("Error", e.getMessage());
            e.printStackTrace();
        }
        return mIcon11;
    }

    protected void onPostExecute(Bitmap result) {
        bmImage.setImageBitmap(result);
    }
}

ユーザーアクティビティを作成する

onCreate() メソッドでは、UserProfileManager からユーザー情報を取得し、その値をビューに設定します。前と同様に、onCreateOptionsMenu() メソッドと onOptionsItemSelected() メソッドが、Toolbarウィジェットのナビゲーション機能を処理します。
package com.auth0.samples.activities;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.ImageView;
import android.widget.TextView;

import com.auth0.samples.R;
import com.auth0.samples.utils.ImageTask;
import com.auth0.samples.utils.UserProfileManager;

public class UserActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.user_activity);
        Toolbar navToolbar = (Toolbar) findViewById(R.id.navToolbar);
        setSupportActionBar(navToolbar);

        TextView tvName = (TextView) findViewById(R.id.tvName);
        TextView tvEmail = (TextView) findViewById(R.id.tvEmail);

        tvName.setText(UserProfileManager.getUserInfo(this).getName());
        tvEmail.setText(UserProfileManager.getUserInfo(this).getEmail());

        new ImageTask((ImageView) findViewById(R.id.ivPicture))
                .execute(UserProfileManager.getUserInfo(this).getPictureURL());

                UserProfileManager.getUserInfo(this).getName();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // アクションバーで使用するメニュー項目をインフレートする
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.user_action_menu, menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_new:
                startActivity(new Intent(UserActivity.this, FormActivity.class));
                break;
            case R.id.action_view:
                startActivity(new Intent(UserActivity.this, TimeSheetActivity.class));
                break;
            default:
                // ここに到達した場合、ユーザーのアクションは認識されなかった。
                // スーパークラスを呼び出して処理する。
                return super.onOptionsItemSelected(item);

        }
        return true;
    }
}

ステップ 7. 新しいタイムシート用フォーム

次に、新しいタイムシートエントリを作成するためのFormActivityとレイアウトを作成します。

マニフェストを更新する

アプリの AndroidManifest.xml を開き、FormActivity を追加します。
<activity android:name="com.auth0.samples.activities.FormActivity" />

フォームアクティビティのレイアウトを作成する

プロジェクト名と作業時間の入力には EditText、作業日の指定には DatePicker を使用した form_activity.xml レイアウトを作成します。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/mainForm"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:weightSum="1">

    <android.support.v7.widget.Toolbar
        android:id="@+id/navToolbar"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        android:elevation="4dp"
        android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/>

    <EditText
        android:id="@+id/editProjectName"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="Project Name"
        android:inputType="textPersonName" />

    <EditText
        android:id="@+id/editHours"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:hint="Hours Worked"
        android:inputType="number|numberDecimal" />

    <DatePicker
        android:id="@+id/datePicker"
        android:layout_width="match_parent"
        android:layout_height="191dp"
        android:layout_weight="0.93" />

    <Button
        android:id="@+id/submitTimeSheetButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Submit" />
</LinearLayout>
FormActivity のToolbar用に、form_actions_menu.xml も作成します。
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/action_profile"
        android:title="Profile"
        app:showAsAction="always" />
    <item
        android:id="@+id/action_view"
        android:title="View Timesheets"
        app:showAsAction="always" />
</menu>

フォームアクティビティを作成する

  • @string/api_urlhttp://10.0.2.2:8080/timesheets に設定されています。これにより、Android Emulator から http://localhost:8080 で実行中の Node.JS API に接続できます。
  • onCreate() メソッドはフォームを初期化し、送信ボタンが押されたときに postAPI() メソッドで使用する入力を取得します。
  • postAPI() メソッドは、フォームから取得したユーザー入力を JSON 形式で Node.JS API に送信します。
  • clearForm() メソッドはフォームの入力内容をクリアします。
  • onCreateOptionsMenu() メソッドと onOptionsItemSelected() メソッドは、Toolbar ウィジェットのナビゲーション機能を処理します。
package com.auth0.samples.activities;

import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.DatePicker;
import android.widget.EditText;
import android.widget.Toast;

import com.auth0.android.Auth0;
import com.auth0.android.authentication.AuthenticationAPIClient;
import com.auth0.android.authentication.storage.CredentialsManager;
import com.auth0.android.authentication.storage.CredentialsManagerException;
import com.auth0.android.authentication.storage.SharedPreferencesStorage;
import com.auth0.android.callback.BaseCallback;
import com.auth0.android.result.Credentials;
import com.auth0.samples.R;
import com.squareup.okhttp.Callback;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.IOException;
import java.util.Calendar;
import java.util.Date;

public class FormActivity extends AppCompatActivity {

    private static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json; charset=utf-8");

    private String accessToken;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.form_activity);
        Toolbar navToolbar = (Toolbar) findViewById(R.id.navToolbar);
        setSupportActionBar(navToolbar);

        Auth0 auth0 = new Auth0(getString(R.string.auth0_client_id), getString(R.string.auth0_domain));
        auth0.setOIDCConformant(true);

        AuthenticationAPIClient authAPIClient = new AuthenticationAPIClient(auth0);
        SharedPreferencesStorage sharedPrefStorage = new SharedPreferencesStorage(this);

        CredentialsManager credentialsManager = new CredentialsManager(authAPIClient, sharedPrefStorage);
        credentialsManager.getCredentials(new BaseCallback<Credentials, CredentialsManagerException>() {
            @Override
            public void onSuccess(Credentials payload) {
                accessToken = payload.getAccessToken();
            }

            @Override
            public void onFailure(CredentialsManagerException error) {
                Toast.makeText(FormActivity.this, "Error: " + error.getMessage(), Toast.LENGTH_SHORT).show();
            }
        });

        Button submitTimeSheetButton = (Button) findViewById(R.id.submitTimeSheetButton);
        final EditText editProjectName = (EditText) findViewById(R.id.editProjectName);
        final EditText editHours = (EditText) findViewById(R.id.editHours);
        final DatePicker datePicker = (DatePicker) findViewById(R.id.datePicker);

        submitTimeSheetButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                int day = datePicker.getDayOfMonth();
                int month = datePicker.getMonth();
                int year =  datePicker.getYear();

                Calendar calendar = Calendar.getInstance();
                calendar.set(year, month, day);

                postAPI(
                        editProjectName.getText().toString(),
                        calendar.getTime(),
                        editHours.getText().toString()
                );
            }
        });
    }

    private void postAPI(String projectName, Date date, String hours) {

        JSONObject postBody = new JSONObject();
        try {
            postBody.put("project", projectName);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        try {
            postBody.put("date", date);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        try {
            postBody.put("hours", hours);
        } catch (JSONException e) {
            e.printStackTrace();
        }
        String postStr = postBody.toString();

        final Request.Builder reqBuilder = new Request.Builder()
                .post(RequestBody.create(MEDIA_TYPE_JSON, postStr))
                .url(getString(R.string.api_url))
                .addHeader("Authorization", "Bearer " + accessToken);

        OkHttpClient client = new OkHttpClient();
        Request request = reqBuilder.build();

        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Request request, IOException e) {
                Log.e("API", "Error: ", e);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        Toast.makeText(FormActivity.this, "An error occurred", Toast.LENGTH_SHORT).show();
                    }
                });
            }

            @Override
            public void onResponse(final Response response) throws IOException {
                final String resBody = response.body().string();

                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (response.isSuccessful()) {
                            clearForm((ViewGroup) findViewById(R.id.mainForm));
                            Intent intent = new Intent(FormActivity.this, TimeSheetActivity.class);
                            FormActivity.this.startActivity(intent);
                        } else {
                            Toast.makeText(FormActivity.this, "Timesheet creation failed.", Toast.LENGTH_SHORT).show();
                        }
                    }
                });
            }
        });
    }

    private void clearForm(ViewGroup group) {
        for (int i = 0, count = group.getChildCount(); i < count; ++i) {
            View view = group.getChildAt(i);
            if (view instanceof EditText) {
                ((EditText)view).setText("");
            }

            if(view instanceof ViewGroup && (((ViewGroup)view).getChildCount() > 0))
                clearForm((ViewGroup)view);
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.form_action_menu, menu);
        return super.onCreateOptionsMenu(menu);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_view:
                startActivity(new Intent(FormActivity.this, TimeSheetActivity.class));
                break;
            case R.id.action_profile:
                startActivity(new Intent(FormActivity.this, UserActivity.class));
                break;
            default:
                return super.onOptionsItemSelected(item);

        }
        return true;
    }
}

アプリをテストする

先に進む前に、必ず Node.JS API を実装 しておいてください。
  1. ターミナルで API のディレクトリに移動し、node server コマンドを実行して API を起動します。
  2. Android Studio でモバイルアプリを開き、Run ボタンを押します。
  3. Nexus 5X API 23 の仮想デバイスを選択します。
  4. エミュレーターでモバイルアプリが起動したら、ユーザーとしてログインし、実行中の Node.JS API を使用してタイムシートエントリを作成・表示できます。
以上で完了です。