ユーザー通知用に Android アプリをクライアント側の SDK と統合する (非推奨)

重要

Microsoft Graph 通知 API は非推奨になり、2022 年 1 月にデータの返送を停止しています。 別の通知エクスペリエンスについては、Microsoft Azure Notification Hubs を参照してください。 詳細については、ブログ記事「Microsoft Graph 通知 API の廃止 (ベータ版)」を参照してください。

アプリをMicrosoft Entra 管理センターに登録し、パートナー デベロッパー センターでクロスデバイス エクスペリエンスをオンボードした後、次の手順は、クライアント アプリを Android 用クライアント側 SDK と統合することです。

クライアント側 SDK を使用することで、アプリは、アプリケーション サーバーから現在サインインしているユーザーを対象に発行された通知の受信を開始するために必要な登録手順を実行できるようになります。 その後で、この SDK はクライアント側での通知の管理を実行します。これには、新しい着信通知の受信、すべて消去のようなシナリオを実現するための通知状態の管理、および完全な通知履歴の取得が含まれます。

新しい着信通知のフロー

新しい受信通知を受け取ると、次の図のようなデータ フローが表示されます。

Android アプリの新しい通知のフロー

プロセスには、いくつかのコンポーネントが含まれます:

  • アプリケーション サーバー - アプリケーションのバック エンド
  • アプリケーション クライアント: アプリケーションのフロント エンド (UWP アプリ、Android アプリ、または iOS アプリ)
  • Microsoft Graph の通知 - デバイスおよびプラットフォームを跨ぐアプリケーション クライアントの異なるインスタンス間でのユーザー通知の発行と格納、同期を可能にするサービス コンポーネント
  • FCM - Firebase Cloud Messaging のことで、Google Play サービスの一部として Android が提供するプッシュ通知サービス。 Microsoft Graph の通知は、このサービスを使用して、ユーザーの通知データの変更について Android アプリ クライアントに通知します。

この図は、次の手順を示しています。

  1. アプリケーションのロジック。 この手順では、ユーザーに通知を発行するトリガーになるものを取り込みます。 これは、アプリケーション固有のロジックであり、Microsoft Graph の別のもの (予定表イベントやタスクの割り当てなど) に関するイベントまたはデータの更新になることも、それとは別にアプリ サービスでユーザーに通知しようとしているものになることもあります。
  2. アプリケーション サーバーから対象ユーザーへの通知は、Microsoft Graph 通知 API 経由で発行されます。 詳細については、サーバー側との統合を参照してください。
  3. 新しい通知が含まれている Web 要求を受信すると、Microsoft Graph 通知は、このアプリとユーザーのためにクラウドで通知の内容を安全に保持します。
  4. このユーザーが通知を受け取るための各アプリケーション クライアント インスタンスのサブスクリプションには、Microsoft Graph の通知がオペレーティング システムによって提供されるネイティブ プッシュ サービスを使用して、アプリのクライアントに通知を送信します。 この場合、アプリケーションは Android アプリであり、FCM データメッセージを使用して信号を送信します。
  5. 受信プッシュ通知によって通知がアプリケーションに送られた後、アプリケーションはユーザー通知ストア内の変更の取得を SDK に求めます。
  6. SDK は、Microsoft Graph 内で、ユーザー通知ストアとの安全で準拠した接続を確立します。
  7. SDK は、データの変更 (この場合は新しい通知の内容) を取得します。
  8. 変更が正常に取得された後、アプリに通知するためのイベント コールバックが SDK で発生します。
  9. アプリケーションのロジック。 この手順では、アプリがイベント コールバック内で実行する内容を取得します。 通常、ローカルのアプリデータが変更され、ローカルの UI 更新が発生します。 この場合、アプリは通常、通知の内容についてユーザーに通知するトースト通知ポップアップを構築します。

通知の更新フロー

Microsoft Graph 通知を使用する主な利点の 1 つは、通知が安全にクラウドで保持され、ステートフル リソース タイプに変更できることです。 そのため、アプリケーションはクロスデバイス シナリオにおいて、サインインしている同一のユーザーに対して、異なるデバイス間で通知の正しい状態を管理および同期できます。 あるデバイスで通知が消去または既読になったときには、その他のデバイスにリアルタイムで通知されます。 "一度処理すれば、すべての場所で消去される" ということが、ユーザーの通知体験の一部として実現されます。

次の図は、通知の状態を変更したり、一つのデバイスでの通知を削除したり、別のデバイスで状態の変更または削除を受信/処理するためのデータフローを示しています。

Android アプリの更新通知のフロー

フローの2番目の部分の通知は、新しい受信通知の処理の流れと似ています。 これは設計上、SDK のプログラミング パターンは、アプリケーション クライアントがすべての種類のユーザー通知データの変更 (新しい受信通知、通知状態の変更、削除された通知) を同様の方法で処理できるように設計されています。

この図は、次の手順を示しています。

  1. アプリケーションのロジック。 何らかのものが、通知の変更または削除をトリガーします。 一般に、あらゆるイベントが通知を変更するトリガーになり得ます。
  2. 通知を更新または削除するクライアント SDK のアプリによる呼び出し。 現在、状態の変更に関して 2 つのプロパティ (userActionState および readState) が公開されていますが、こうした状態とその状態の更新時期はアプリケーションで定義できます。 たとえば、ユーザーが通知ポップアップを消去したときには、userActionState が消去済み (Dismissed) になるように更新します。 ユーザーが通知ポップアップをクリックして、それに対応するアプリのコンテンツを利用するアプリを起動したときに、userActionState をアクティブ化済み (Activated) に更新し、readState を読み取り (Read) に更新します。
  3. 通知を更新または削除するために対応する API が呼び出された後、SDK は、クラウド内のユーザー通知ストアに送信して、この変更をサインインしている同一ユーザーの別のアプリクライアント インスタンスに展開します。
  4. クライアントから更新/削除要求を受信した Microsoft Graph 通知は、通知ストアを更新して、この変更をサブスクライブしている別のアプリ クライアント インスタンスを特定します。
  5. 各アプリケーションの クライアント サブスクリプションには、Microsoft Graph の通知がオペレーティング システムによって提供されるネイティブ プッシュ サービスを使用して、アプリのクライアントに通知を送信します。 この場合、これは Android アプリであり、FCM データメッセージを使用して信号を送信します。
  6. 受信プッシュ通知によって通知がアプリケーションに送られた後、アプリケーションはユーザー通知ストア内の変更の取得を SDK に求めます。
  7. SDK は、Microsoft Graph 内で、ユーザー通知ストアとの安全で準拠した接続を確立します。
  8. SDK によってデータが変更されます。ここでは、変更内容は通知状態の更新または通知の削除です。
  9. 変更が正常に取得された後、アプリに通知するためのイベント コールバックが SDK で発生します。
  10. アプリケーションのロジック。 この手順では、アプリがイベント コールバック内で実行する内容を取得します。 通常、ローカルのアプリデータが変更され、ローカルの UI 更新が発生します。 この場合、通知の更新があるため、アプリはローカルに UI を更新して状態の変更を反映させる必要があります。 たとえば、通知がアクティブ化されている場合、Android 通知トレイ内の対応する通知メッセージを削除すると、"一度処理すれば、すべての場所で消去" することができます。

Microsoft Graph の通知の詳細については、Microsoft Graph の通知の概要を参照してください。 全てを Microsoft Graph の通知と統合するために必要な手順の詳細については、Microsoft Graph の通知の統合の概要を参照してください。

開発環境と要件

Microsoft Graph の通知を使用するには、Android アプリ開発 IDE と、サポートされているアーキテクチャの 1 つ (armeabi-v7aarm64-v8ax86、または x86_64) を備えた Android デバイス、またはエミュレーターが必要です。 システムは Android 4.4.2 以降を実行している必要があります。

プロジェクトに SDK を追加する

プロジェクトのルートにある build.gradle ファイルに、次のリポジトリ参照を挿入します。

allprojects {
    repositories {
    jcenter()
    maven { url 'https://maven.google.com' }
    maven { url 'https://projectrome.bintray.com/maven/' }
    }
}

次に、プロジェクトフォルダーにある build.gradle ファイルに次の依存関係を挿入します。

dependencies { 
    ...
    implementation 'com.microsoft.connecteddevices:connecteddevices-sdk:+'
}

アプリで ProGuard を使用したい場合は、これらの新しい API の ProGuard ルールを追加してください。 プロジェクトの App フォルダーに proguard-rules.txt という名前のファイルを作成し、 のProGuard_Rules_for_Android_Rome_SDK.txt内容を貼り付けます。 プロジェクトの AndroidManifest.xml ファイルで、manifest 要素内に次の権限を追加します (まだ存在しない場合)。 これにより、インターネットに接続してデバイス上で Bluetooth 検出を有効にする権限がアプリケーションに与えられます。 Bluetooth 関連の権限は、Bluetooth 検出を使用する場合にのみ必要であり、接続されているデバイス プラットフォームの他の機能には必要ありません。 また、ACCESS_COARSE_LOCATIONは Android SDK 21 以降でのみ必須です。 Android SDK 23 以降では、実行時に位置アクセスを許可するようにユーザーに要求する必要もあります。

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

次に、接続されているデバイスの機能を配置するアクティビティのクラスに移動します。 次の名前空間をインポートします。

import com.microsoft.connecteddevices;
import com.microsoft.connecteddevices.userdata;
import com.microsoft.connecteddevices.userdata.usernotifications;

Connected Device Platforms の初期化

クライアント側のSDKは、Connected Device Platforms と呼ばれるインフラストラクチャの上に構築されています。 機能を使用するには、アプリ内でプラットフォームを初期化する必要があります。 この初期化手順は、通知シナリオを実行する前に必須であるため、メインクラス OnCreate メソッド内で実行する必要があります。

ConnectedDevicesPlatform クラスをインスタンス化して、プラットフォームを構築および初期化する必要があります。 これを行う前に、プラットフォームが起動した後、イベントが発生する可能性があるため、イベント ハンドラーを接続してください。

ConnectedDevicesPlatform platform = new ConnectedDevicesPlatform(context);

platform.getAccountManager().accessTokenRequested().subscribe((accountManager, args) -> onAccessTokenRequested(accountManager, args));
platform.getAccountManager().accessTokenInvalidated().subscribe((accountManager, args) -> onAccessTokenInvalidated(accountManager, args));
platform.getNotificationRegistrationManager().notificationRegistrationStateChanged().subscribe((notificationRegistrationManager, args) -> onNotificationRegistrationStateChanged(notificationRegistrationManager, args));

platform.start();

アカウント アクセス トークンの処理

新しい着信通知のコンテンツの取得、通知状態の更新など、SDK で実行されるすべての Web 呼び出しは、ユーザーのデータに対する読み取りと書き込みになるため、常に有効なアクセス トークンが必要になります。 プラットフォームが初期化された後に、ユーザーのアクセストークンが正常に機能するために、SDK は次のイベント - アクセス トークン が要求または無効化されたときに呼び出された - の処理を必要とします。

accessTokenRequested

完全に実装するには、Android サンプルアプリを参照してください。

private void onAccessTokenRequested(ConnectedDevicesAccountManager sender, ConnectedDevicesAccessTokenRequestedEventArgs args) {
    ConnectedDevicesAccessTokenRequest request = args.getRequest();
    List<String> scopes = request.getScopes();

    // We always need to complete the request, even if a matching account is not found
    if (account == null) {
        request.completeWithErrorMessage("The app could not find a matching ConnectedDevicesAccount to get a token");
        return;
    }

    // Complete the request with a token
    account.getAccessTokenAsync(scopes)
        .thenAcceptAsync((String token) -> {
            request.completeWithAccessToken(token);
        }).exceptionally(throwable -> {
            request.completeWithErrorMessage("The Account could not return a token with those scopes");
            return null;
    });
}

accessTokenInvalidated

完全に実装するには、Android サンプルアプリを参照してください。

private void onAccessTokenInvalidated(ConnectedDevicesAccountManager sender, ConnectedDevicesAccessTokenInvalidatedEventArgs args, List<Account> accounts) {
    Log.i(TAG, "Token invalidated for account: " + args.getAccount().getId());
}

プッシュ登録の有効期限を処理する

Microsoft Graph の通知は、FCM、Androidのネイティブのプッシュ プラットフォームを使用して、ユーザー通知のデータ変更をクライアント アプリケーションに通知します。 これは、新しい受信通知がアプリサーバーから発行されている場合、またはクロスデバイス シナリオにおいてサインインした同じユーザーの別のデバイスで通知の状態が更新された場合に発生します。

したがって、データ通知メッセージが正常に送信されることを許可する有効なFCMトークンが必要です。 次のイベントコールバックでは、FCM プッシュトークンの有効期限を処理します。

notificationRegistrationStateChanged

完全に実装するには、Android サンプルアプリを参照してください。

ユーザーにサインインする

Microsoft Graph 内の他の多くのリソースタイプと同様に、Microsoft Graph の通知は、ユーザーを中核として一元化されます。 アプリでサブスクライブし、サインインしたユーザーへの通知を受信できるようにするには、最初に登録プロセスで使用する有効な OAuth トークンを入手する必要があります。 OAuth トークンを生成および管理する方法を選択できます。 サンプルアプリは ADAL を使用しました。

Microsoft アカウント を使用している場合は、サインイン リクエストに次の許可を含める必要があります:wl.offline_access", ccs.ReadWrite, wns.connect, asimovrome.telemetry, https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp

Microsoft Entra アカウントを使用している場合は、次の対象ユーザーを要求する必要があります。 https://cdpcs.access.microsoft.com

プラットフォームにユーザーアカウントを追加する

サインインしたユーザーアカウントを SDK に登録する必要があります。これには、FCM を経由して最初のプッシュ通知を受信するためのアカウントの追加とプッシュ チャネルの登録が含まれます。

public AsyncOperation<Boolean> prepareAccountAsync(final Context context) {
    // Accounts can be in 3 different scenarios:
    // 1: cached account in good standing (initialized in the SDK and our token cache).
    // 2: account missing from the SDK but present in our cache: Add and initialize account.
    // 3: account missing from our cache but present in the SDK. Log the account out async

    // Subcomponents (e.g. UserDataFeed) can only be initialized when an account is in both the app cache
    // and the SDK cache.
    // For scenario 1, initialize our subcomponents.
    // For scenario 2, subcomponents will be initialized after InitializeAccountAsync registers the account with the SDK.
    // For scenario 3, InitializeAccountAsync will unregister the account and subcomponents will never be initialized.
    switch (mState) {
        // Scenario 1
        case IN_APP_CACHE_AND_SDK_CACHE:
            mUserNotificationsManager = new UserNotificationsManager(context, mAccount, mPlatform);
            return registerAccountWithSdkAsync();
        // Scenario 2
        case IN_APP_CACHE_ONLY: {
            // Add the this account to the ConnectedDevicesPlatform.AccountManager
            return mPlatform.getAccountManager().addAccountAsync(mAccount).thenComposeAsync((ConnectedDevicesAddAccountResult result) -> {
                // We failed to add the account, so exit with a failure to prepare bool
                if (result.getStatus() != ConnectedDevicesAccountAddedStatus.SUCCESS) {
                    result.getStatus());
                    return AsyncOperation.completedFuture(false);
                }

                // Set the registration state of this account as in both app and sdk cache
                mState = AccountRegistrationState.IN_APP_CACHE_AND_SDK_CACHE;
                mUserNotificationsManager = new UserNotificationsManager(context, mAccount, mPlatform);
                return registerAccountWithSdkAsync();
            });
        }
        // Scenario 3
        case IN_SDK_CACHE_ONLY:
            // Remove the account from the SDK since the app has no knowledge of it
            mPlatform.getAccountManager().removeAccountAsync(mAccount);
            // This account could not be prepared
            return AsyncOperation.completedFuture(false);
        default:
            // This account could not be prepared
            Log.e(TAG, "Failed to prepare account " + mAccount.getId() + " due to unknown state!");
            return AsyncOperation.completedFuture(false);
    }
}
public AsyncOperation<Boolean> registerAccountWithSdkAsync() {
    if (mState != AccountRegistrationState.IN_APP_CACHE_AND_SDK_CACHE) {
        AsyncOperation<Boolean> toReturn = new AsyncOperation<>();
        toReturn.completeExceptionally(new IllegalStateException("Cannot register this account due to bad state: " + mAccount.getId()));
        return toReturn;
    }

    // Grab the shared GCM/FCM notification token from this app's BroadcastReceiver
    return RomeNotificationReceiver.getNotificationRegistrationAsync().thenComposeAsync((ConnectedDevicesNotificationRegistration notificationRegistration) -> {
        // Perform the registration using the NotificationRegistration
        return mPlatform.getNotificationRegistrationManager().registerAsync(mAccount, notificationRegistration)
            .thenComposeAsync((result) -> {
                if (result.getStatus() == ConnectedDevicesNotificationRegistrationStatus.SUCCESS) {
                    Log.i(TAG, "Successfully registered account " + mAccount.getId() + " for cloud notifications");
                } else {
                    // It would be a good idea for apps to take a look at the different statuses here and perhaps attempt some sort of remediation.
                    // For example, token request failed could mean that the user needs to sign in again. An app could prompt the user for this action 
                    // and retry the operation afterwards.
                    Log.e(TAG, "Failed to register account " + mAccount.getId() + " for cloud notifications!");
                    return AsyncOperation.completedFuture(false);
                }

                return mUserNotificationsManager.registerForAccountAsync();
            });
    });
}

ユーザーの通知を受信するためのサブスクライブ

このログイン ユーザーのアプリケーションのため、UserDataFeed オブジェクトをインスタンス化する必要があります。 アプリケーションは、クロスデバイス エクスペリエンスの開始に指定したクロスプラットフォームアプリ ID によって識別されます。

public UserNotificationsManager(@NonNull Context context, @NonNull ConnectedDevicesAccount account, @NonNull ConnectedDevicesPlatform platform)
{
    Context context = new Context;
    UserDataFeed feed = UserDataFeed.getForAccount(account, platform, Secrets.APP_HOST_NAME);
    UserNotificationChannel channel = new UserNotificationChannel(feed);
    UserNotificationReader reader = channel.createReader();
    reader.dataChanged().subscribe((reader, aVoid) -> readFromCache(reader));
    }
}

ユーザー通知を受信および管理する

このトピックの前半のフロー図では、アプリ サーバーからの新しい着信通知や、別のアプリケーション クライアント インスタンスで開始された通知の更新または削除を処理するためのプログラミング パターンを示しました。 このようなデータの変更を処理する手順は次のとおりです。

着信プッシュ通知シグナルの処理

全ての種類のユーザー通知データの変更について、プッシュ通知としてアプリクライアントに配信されるシグナルが生成されます。 Android アプリの場合、信号は FCM プッシュ データ メッセージとして配信されます。 データ メッセージ シグナルを受信するには、アプリがTryParseを呼び出し、実際のデータ変更のためのSDKによる Microsoft Graph の通知サービスからの取得をトリガーします。

public void onMessageReceived(RemoteMessage message) {
    Map data = message.getData();
    ConnectedDevicesNotification notification = ConnectedDevicesNotification.tryParse(data);

    if (notification != null) {
        try {
            ConnectedDevicesPlatform platform = ConnectedDevicesManager.getConnectedDevicesManager(getApplicationContext()).getPlatform();

            // NOTE: it may be useful to attach completion to this async in order to know when the notification is done being processed.
            // This would be a good time to stop a background service or otherwise cleanup.
            platform.processNotificationAsync(notification);
        } catch (Exception e) {
            Log.e(TAG, "Failed to process FCM notification" + e.getMessage());
        }
    }
}

ユーザー通知のデータ変更の処理

SDK がデータ変更の取得を正常に完了すると、イベントコールバックが起こり、アプリクライアントが通知の作成、更新、または削除を処理することが想定されます。

private void readFromCache(final UserNotificationReader reader)
{
    reader.readBatchAsync(Long.MAX_VALUE).thenAccept(notifications -> {
        synchronized (this) {
            for (final UserNotification notification : notifications) {
                if (notification.getStatus() == UserNotificationStatus.ACTIVE) {
                    removeIf(mNewNotifications, item -> notification.getId().equals(item.getId()));

                    if (notification.getUserActionState() == UserNotificationUserActionState.NO_INTERACTION) {
                        mNewNotifications.add(notification);
                        if (notification.getReadState() != UserNotificationReadState.READ) {
                            clearNotification(mContext.getApplicationContext(), notification.getId());
                            addNotification(mContext.getApplicationContext(), notification.getContent(), notification.getId());
                        }
                    } else {
                        clearNotification(mContext.getApplicationContext(), notification.getId());
                    }

                    removeIf(mHistoricalNotifications, item -> notification.getId().equals(item.getId()));
                    mHistoricalNotifications.add(0, notification);
                } else {
                    removeIf(mNewNotifications, item -> notification.getId().equals(item.getId()));
                    removeIf(mHistoricalNotifications, item -> notification.getId().equals(item.getId()));
                    clearNotification(mContext.getApplicationContext(), notification.getId());
                }
            }
        }

    });
}

通知の更新状態

このアプリ クライアントのインスタンスから通知状態の変更が開始された場合 (たとえば、このデバイスのトースト通知ポップアップをユーザーが有効化した場合)、アプリでは、この状態の変更を同じユーザーが使用するすべてのデバイスで同期するために、SDK を呼び出して通知の状態を更新する必要があります。

notification.setUserActionState(UserNotificationUserActionState.ACTIVATED);
notification.saveAsync().whenCompleteAsync((userNotificationUpdateResult, throwable) -> {
    if (throwable == null && userNotificationUpdateResult != null && userNotificationUpdateResult.getSucceeded()) {
        Log.d(TAG, "Successfully activated the notification");
    }
});

通知を削除する

このアプリ クライアントのインスタンスから通知の削除が開始された場合 (たとえば、この通知に対応するタスクに完了のマークが付けられ、アプリのデータベースから通知が削除された場合)、アプリでは、この削除の操作を同じユーザーが使用するすべてのデバイスで同期するために、SDK を呼び出して通知を削除する必要があります。

通知は、有効期限が切れているか、明示的に削除された場合にのみ、ユーザー通知ストアから削除されます。 UserActionState のセマンティック定義はアプリケーション自体によって定義されているため、UserActionState が消去済み (Dismissed) になるように更新しても、ユーザー通知は削除されません。

channel.deleteUserNotificationAsync(notification.getId()).whenCompleteAsync((userNotificationUpdateResult, throwable) -> {
    if (throwable == null && userNotificationUpdateResult != null && userNotificationUpdateResult.getSucceeded()) {
        Log.d(TAG, "Successfully deleted the notification");
    }
});