これらのセクションは参考情報であり、上から下まで読む必要はありません。
ユーザーの同意を求める
フレームワーク API を使用する:
CrossProfileApps.canInteractAcrossProfiles()
CrossProfileApps.canRequestInteractAcrossProfiles()
CrossProfileApps.createRequestInteractAcrossProfilesIntent()
CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED
これらの API は、より一貫性のある API サーフェス(UserHandle オブジェクトの回避など)のために SDK でラップされますが、現時点では直接呼び出すことができます。
実装は簡単です。操作できる場合は、そのまま操作します。リクエストできるが、リクエストを送信できない場合は、ユーザーにプロンプト / バナー / ツールチップなどを表示します。ユーザーが設定に移動することに同意した場合は、リクエスト インテントを作成し、Context#startActivity
を使用してユーザーを設定に移動させます。ブロードキャストを使用してこの機能が変更されたことを検出するか、ユーザーが戻ってきたときにもう一度確認します。
これをテストするには、仕事用プロファイルで TestDPC を開き、一番下までスクロールして、接続済みアプリの許可リストにパッケージ名を追加するように選択します。これは、管理者がアプリを「許可リストに登録」するのと同じ効果があります。
用語集
このセクションでは、クロス プロファイル開発に関連する主な用語を定義します。
クロス プロファイルの設定
クロス プロファイル構成は、関連するクロス プロファイル プロバイダ クラスをグループ化し、クロス プロファイル機能の一般的な構成を提供します。通常、コードベースごとに 1 つの @CrossProfileConfiguration
アノテーションがありますが、複雑なアプリケーションでは複数になることがあります。
プロフィール コネクタ
コネクタは、プロファイル間の接続を管理します。通常、各クロス プロファイル タイプは特定のコネクタを参照します。1 つの構成内のすべてのクロス プロファイル タイプで、同じコネクタを使用する必要があります。
クロス プロファイル プロバイダ クラス
クロス プロファイル プロバイダ クラスは、関連するクロス プロファイル タイプをグループ化します。
Mediator
メディエーターは、高レベル コードと低レベル コードの間にあり、正しいプロファイルに呼び出しを分散し、結果を統合します。これは、プロファイル対応にする必要がある唯一のコードです。これは、SDK に組み込まれているものではなく、アーキテクチャのコンセプトです。
クロス プロファイルの種類
クロス プロファイル タイプは、@CrossProfile
アノテーションが付けられたメソッドを含むクラスまたはインターフェースです。このタイプのコードはプロファイル対応である必要はなく、ローカルデータに対してのみ動作するのが理想的です。
プロファイルの種類
プロファイルの種類 | |
---|---|
現在 | 実行中のアクティブなプロファイル。 |
その他 | (存在する場合)実行されていないプロファイル。 |
パーソナル | ユーザー 0: デバイス上のプロフィールで、オフにすることはできません。 |
職場 | 通常はユーザー 10 ですが、それ以上の場合もあります。オンとオフを切り替えることができ、仕事用のアプリとデータを格納するために使用されます。 |
プライマリ | 必要に応じてアプリで定義します。両方のプロファイルの統合ビューを表示するプロファイル。 |
セカンダリ | primary が定義されている場合、secondary はプライマリではないプロファイルです。 |
サプライヤー | プライマリ プロファイルのサプライヤーは両方のプロファイルで、セカンダリ プロファイルのサプライヤーはセカンダリ プロファイル自体のみです。 |
プロファイル ID
プロファイルのタイプ(個人用または仕事用)を表すクラス。これらは、複数のプロファイルで実行されるメソッドによって返され、それらのプロファイルでさらにコードを実行するために使用できます。これらは int
にシリアル化され、保存が容易になります。
アーキテクチャに関する推奨ソリューション
このガイドでは、Android アプリ内で効率的でメンテナンス可能なクロス プロファイル機能を構築するための推奨構造について説明します。
CrossProfileConnector
をシングルトンに変換する
アプリケーションのライフサイクル全体で使用するのは 1 つのインスタンスのみにしてください。複数のインスタンスを使用すると、並列接続が作成されます。これは、Dagger などの依存関係インジェクション フレームワークを使用するか、新しいクラスまたは既存のクラスで従来のシングルトン パターンを使用するかによって行えます。
生成された Profile インスタンスをメソッドで作成するのではなく、呼び出し時にクラスに挿入または渡す
これにより、後で単体テストで自動生成された FakeProfile
インスタンスを渡すことができます。
メディエーター パターンを検討する
この一般的なパターンでは、既存の API のいずれか(getEvents()
など)を、すべての呼び出し元に対してプロファイル対応にします。この場合、既存の API は、生成されたクロス プロファイル コードへの新しい呼び出しを含む「メディエーター」メソッドまたはクラスにすることができます。
これにより、すべての呼び出し元がクロス プロファイル呼び出しを行う必要がなくなります。これは API の一部になります。
プロバイダで実装クラスを公開しないように、インターフェース メソッドに @CrossProfile
としてアノテーションを付けることを検討する
これは、依存関係挿入フレームワークとうまく連携します。
クロス プロファイル呼び出しからデータを受信している場合は、データの送信元プロファイルを参照するフィールドを追加することを検討してください。
これは、UI レイヤでこの情報を把握したい場合(仕事用アイテムにバッジ アイコンを追加する場合など)に適した方法です。また、パッケージ名など、データ ID が一意でなくなる場合は、必須になることもあります。
クロス プロファイル
このセクションでは、独自のクロス プロファイル インタラクションを作成する方法について説明します。
メイン プロファイル
このドキュメントの例の呼び出しのほとんどには、仕事用、個人用、両方など、どのプロファイルで実行するかに関する明示的な手順が含まれています。
実際には、1 つのプロファイルでのみ統合されたエクスペリエンスを持つアプリでは、この決定を実行しているプロファイルに依存させることが一般的です。そのため、コードベースに if-else プロファイル条件が散らばらないように、これも考慮した便利なメソッドがあります。
コネクタ インスタンスを作成するときに、どのプロファイル タイプを「プライマリ」にするか(例:「WORK」)を指定できます。これにより、次のような追加オプションが有効になります。
profileCalendarDatabase.primary().getEvents();
profileCalendarDatabase.secondary().getEvents();
// Runs on all profiles if running on the primary, or just
// on the current profile if running on the secondary.
profileCalendarDatabase.suppliers().getEvents();
クロス プロファイルの種類
@CrossProfile
アノテーション付きのメソッドを含むクラスとインターフェースは、クロス プロファイル タイプと呼ばれます。
クロス プロファイル タイプの実装は、実行されているプロファイルから独立している必要があります。他のメソッドを呼び出すことは許可されており、通常は単一のプロファイルで実行されているように動作します。アクセスできるのは、自分のプロフィールの状態のみです。
クロス プロファイル タイプの例:
public class Calculator {
@CrossProfile
public int add(int a, int b) {
return a + b;
}
}
クラスのアノテーション
最も強力な API を提供するには、次のように、各クロス プロファイル タイプにコネクタを指定する必要があります。
@CrossProfile(connector=MyProfileConnector.class)
public class Calculator {
@CrossProfile
public int add(int a, int b) {
return a + b;
}
}
これは省略可能ですが、生成される API の型がより具体的になり、コンパイル時チェックが厳格になります。
インターフェース
インターフェースのメソッドに @CrossProfile
としてアノテーションを付けると、プロファイル間でアクセスできるこのメソッドの実装が存在する可能性があることを示します。
クロス プロファイル プロバイダでクロス プロファイル インターフェースの実装を返すことができます。これにより、この実装にクロス プロファイルでアクセスできることを宣言します。実装クラスにアノテーションを付ける必要はありません。
クロス プロファイル プロバイダ
すべてのクロス プロファイル タイプは、@CrossProfileProvider
アノテーションが付いたメソッドによって提供する必要があります。これらのメソッドは、クロスプロファイル呼び出しが行われるたびに呼び出されるため、各タイプにシングルトンを維持することをおすすめします。
コンストラクタ
プロバイダには、引数なしまたは単一の Context
引数を取るパブリック コンストラクタが必要です。
プロバイダの方法
プロバイダ メソッドは、引数を指定しないか、単一の Context
引数を指定する必要があります。
依存関係の挿入
Dagger などの依存関係の挿入フレームワークを使用して依存関係を管理している場合は、通常どおりにフレームワークでクロス プロファイル タイプを作成し、それらのタイプをプロバイダ クラスに挿入することをおすすめします。@CrossProfileProvider
メソッドは、挿入されたインスタンスを返すことができます。
プロフィール コネクタ
各クロス プロファイル構成には、他のプロファイルへの接続を管理する単一のプロファイル コネクタが必要です。
デフォルト プロファイル コネクタ
コードベースにクロス プロファイル構成が 1 つしかない場合は、独自のプロファイル コネクタを作成せずに com.google.android.enterprise.connectedapps.CrossProfileConnector
を使用できます。指定しない場合のデフォルトです。
クロス プロファイル コネクタを作成するときに、ビルダーで次のオプションを指定できます。
スケジュール実行サービス
SDK によって作成されたスレッドを制御する場合は、
#setScheduledExecutorService()
を使用します。Binder
プロファイル バインディングに関する特定のニーズがある場合は、
#setBinder
を使用します。これは、Device Policy Controller でのみ使用される可能性があります。
カスタム プロファイル コネクタ
一部の構成(CustomProfileConnector
を使用)を設定するには、カスタム プロファイル コネクタが必要です。また、1 つのコードベースで複数のコネクタが必要な場合(複数のプロセスがある場合など)にも、カスタム プロファイル コネクタが必要です。プロセスごとに 1 つのコネクタを推奨します。
ProfileConnector
を作成する場合は、次のようにします。
@GeneratedProfileConnector
public interface MyProfileConnector extends ProfileConnector {
public static MyProfileConnector create(Context context) {
// Configuration can be specified on the builder
return GeneratedMyProfileConnector.builder(context).build();
}
}
serviceClassName
生成されるサービスの名前(
AndroidManifest.xml
で参照される名前)を変更するには、serviceClassName=
を使用します。primaryProfile
プライマリ プロファイルを指定するには、
primaryProfile
を使用します。availabilityRestrictions
SDK が接続とプロファイルの可用性に適用する制限を変更するには、
availabilityRestrictions
を使用します。
Device Policy Controller
アプリが Device Policy Controller の場合は、DeviceAdminReceiver
を参照する DpcProfileBinder
のインスタンスを指定する必要があります。
独自のプロフィール コネクタを実装する場合:
@GeneratedProfileConnector
public interface DpcProfileConnector extends ProfileConnector {
public static DpcProfileConnector get(Context context) {
return GeneratedDpcProfileConnector.builder(context).setBinder(new
DpcProfileBinder(new ComponentName("com.google.testdpc",
"AdminReceiver"))).build();
}
}
または、デフォルトの CrossProfileConnector
を使用します。
CrossProfileConnector connector =
CrossProfileConnector.builder(context).setBinder(new DpcProfileBinder(new
ComponentName("com.google.testdpc", "AdminReceiver"))).build();
クロス プロファイルの設定
@CrossProfileConfiguration
アノテーションは、メソッド呼び出しを正しくディスパッチするために、コネクタを使用してすべてのクロス プロファイル タイプをリンクするために使用されます。そのためには、すべてのプロバイダを指す @CrossProfileConfiguration
をクラスにアノテーションします。次に例を示します。
@CrossProfileConfiguration(providers = {TestProvider.class})
public abstract class TestApplication {
}
これにより、すべてのクロス プロファイル タイプで、同じプロファイル コネクタが指定されているか、コネクタが指定されていないことが検証されます。
serviceSuperclass
デフォルトでは、生成されたサービスは
android.app.Service
をスーパークラスとして使用します。別のクラス(それ自体がandroid.app.Service
のサブクラスである必要があります)をスーパークラスにする場合、serviceSuperclass=
を指定します。serviceClass
指定すると、サービスは生成されません。これは、使用しているプロファイル コネクタの
serviceClassName
と一致している必要があります。カスタム サービスは、次のように生成された_Dispatcher
クラスを使用して呼び出しをディスパッチする必要があります。
public final class TestProfileConnector_Service extends Service {
private Stub binder = new Stub() {
private final TestProfileConnector_Service_Dispatcher dispatcher = new
TestProfileConnector_Service_Dispatcher();
@Override
public void prepareCall(long callId, int blockId, int numBytes, byte[] params)
{
dispatcher.prepareCall(callId, blockId, numBytes, params);
}
@Override
public byte[] call(long callId, int blockId, long crossProfileTypeIdentifier,
int methodIdentifier, byte[] params,
ICrossProfileCallback callback) {
return dispatcher.call(callId, blockId, crossProfileTypeIdentifier,
methodIdentifier, params, callback);
}
@Override
public byte[] fetchResponse(long callId, int blockId) {
return dispatcher.fetchResponse(callId, blockId);
};
@Override
public Binder onBind(Intent intent) {
return binder;
}
}
これは、クロス プロファイル呼び出しの前後に追加のアクションを実行する必要がある場合に使用できます。
コネクタ
デフォルトの
CrossProfileConnector
以外のコネクタを使用している場合は、connector=
を使用して指定する必要があります。
公開設定
プロファイル間でやり取りするアプリのすべての部分で、プロファイル コネクタを表示できる必要があります。
@CrossProfileConfiguration
アノテーション付きのクラスは、アプリで使用されているすべてのプロバイダを参照できる必要があります。
同期呼び出し
Connected Apps SDK は、同期(ブロック)呼び出しをサポートしています。これは、同期呼び出しを回避できない場合に使用します。ただし、これらの呼び出しには、呼び出しが長時間ブロックされる可能性があるなど、いくつかの欠点があるため、可能であれば同期呼び出しを避けることをおすすめします。非同期呼び出しの使用については、非同期呼び出しをご覧ください。
接続ホルダー
同期呼び出しを使用している場合は、クロス プロファイル呼び出しを行う前に、接続ホルダーが登録されていることを確認する必要があります。登録されていないと、例外がスローされます。詳細については、接続ホルダーをご覧ください。
接続ホルダーを追加するには、任意のオブジェクト(クロスプロファイル呼び出しを行うオブジェクト インスタンス)を使用して ProfileConnector#addConnectionHolder(Object)
を呼び出します。これにより、このオブジェクトが接続を使用していることが記録され、接続が試行されます。これは、同期呼び出しを行う前に呼び出す必要があります。これは非ブロッキング呼び出しであるため、呼び出しを行うまでに接続が準備されていない(または接続できない)可能性があります。その場合は、通常のエラー処理動作が適用されます。
ProfileConnector#addConnectionHolder(Object)
を呼び出すときに適切なクロス プロファイル権限がない場合や、接続できるプロファイルがない場合、エラーはスローされませんが、接続コールバックは呼び出されません。後で権限が付与された場合や、他のプロファイルが使用可能になった場合は、接続が確立され、コールバックが呼び出されます。
一方、ProfileConnector#connect(Object)
はブロック方法で、オブジェクトを接続ホルダーとして追加し、接続を確立するか UnavailableProfileException
をスローします。このメソッドは UI スレッド
から呼び出すことはできません。
ProfileConnector#connect(Object)
や同様の ProfileConnector#connect
の呼び出しは、自動クローズ オブジェクトを返します。このオブジェクトは、クローズされると接続ホルダーを自動的に削除します。これにより、次のような使用が可能になります。
try (ProfileConnectionHolder p = connector.connect()) {
// Use the connection
}
同期呼び出しが完了したら、ProfileConnector#removeConnectionHolder(Object)
を呼び出す必要があります。すべての接続ホルダーが削除されると、接続は閉じられます。
接続
接続リスナーを使用すると、接続状態が変化したときに通知を受け取ることができます。また、connector.utils().isConnected
を使用すると、接続が存在するかどうかを判断できます。次に例を示します。
// Only use this if using synchronous calls instead of Futures.
crossProfileConnector.connect(this);
crossProfileConnector.registerConnectionListener(() -> {
if (crossProfileConnector.utils().isConnected()) {
// Make cross-profile calls.
}
});
非同期呼び出し
プロファイル分割で公開されるすべてのメソッドは、ブロッキング(同期)またはノンブロッキング(非同期)として指定する必要があります。非同期データ型(ListenableFuture
など)を返すメソッドや、コールバック パラメータを受け取るメソッドは、ノンブロッキングとしてマークされます。他のすべてのメソッドはブロックとしてマークされます。
非同期呼び出しをおすすめします。同期呼び出しを使用する必要がある場合は、同期呼び出しをご覧ください。
コールバック
非ブロッキング呼び出しの最も基本的なタイプは、結果で呼び出されるメソッドを含むインターフェースをパラメータの 1 つとして受け取る void メソッドです。これらのインターフェースを SDK で動作させるには、インターフェースに @CrossProfileCallback
アノテーションを付ける必要があります。次に例を示します。
@CrossProfileCallback
public interface InstallationCompleteListener {
void installationComplete(int state);
}
このインターフェースは、@CrossProfile
アノテーション付きメソッドのパラメータとして使用し、通常どおり呼び出すことができます。次に例を示します。
@CrossProfile
public void install(String filename, InstallationCompleteListener callback) {
// Do something on a separate thread and then:
callback.installationComplete(1);
}
// In the mediator
profileInstaller.work().install(filename, (status) -> {
// Deal with callback
}, (exception) -> {
// Deal with possibility of profile unavailability
});
このインターフェースに、0 個または 1 個のパラメータを受け取る単一のメソッドが含まれている場合は、複数のプロファイルを一度に呼び出す際にも使用できます。
コールバックを使用して任意の数の値を渡すことができますが、接続は最初の値に対してのみ保持されます。接続を開いたままにして値を受け取る方法については、接続ホルダをご覧ください。
コールバックを使用した同期メソッド
SDK でコールバックを使用する際の珍しい機能の 1 つは、技術的にはコールバックを使用する同期メソッドを記述できることです。
public void install(InstallationCompleteListener callback) {
callback.installationComplete(1);
}
この場合、コールバックにもかかわらず、メソッドは実際には同期的です。このコードは正しく実行されます。
System.out.println("This prints first");
installer.install(() -> {
System.out.println("This prints second");
});
System.out.println("This prints third");
ただし、SDK を使用して呼び出すと、動作は異なります。「This prints third」が印刷される前に、インストール メソッドが呼び出されるとは限りません。SDK によって非同期としてマークされたメソッドを使用する場合は、メソッドが呼び出されるタイミングを想定しないでください。
シンプルなコールバック
「シンプルなコールバック」は、より制限の厳しいコールバック形式で、クロス プロファイル呼び出し時に追加機能を使用できます。単純なインターフェースには、ゼロまたは 1 つのパラメータを受け取ることができる単一のメソッドを含める必要があります。
@CrossProfileCallback
アノテーションで simple=true
を指定すると、コールバック インターフェースを保持する必要があります。
シンプルなコールバックは、.both()
、.suppliers()
などのさまざまなメソッドで使用できます。
接続ホルダー
非同期呼び出し(コールバックまたはフューチャーを使用)を行うと、呼び出し時に接続ホルダーが追加され、例外または値が渡されると削除されます。
コールバックを使用して複数の結果が渡されることが予想される場合は、コールバックを手動で接続ホルダーとして追加する必要があります。
MyCallback b = //...
connector.addConnectionHolder(b);
profileMyClass.other().registerListener(b);
// Now the connection will be held open indefinitely, once finished:
connector.removeConnectionHolder(b);
これは、try-with-resources ブロックでも使用できます。
MyCallback b = //...
try (ProfileConnectionHolder p = connector.addConnectionHolder(b)) {
profileMyClass.other().registerListener(b);
// Other things running while we expect results
}
コールバックまたはフューチャーを使用して呼び出しを行うと、結果が渡されるまで接続が開いたままになります。結果が渡されないと判断した場合は、コールバックまたはフューチャーを接続保持者として削除する必要があります。
connector.removeConnectionHolder(myCallback);
connector.removeConnectionHolder(future);
詳細については、接続ホルダーをご覧ください。
Futures
フューチャーも SDK でネイティブにサポートされています。ネイティブでサポートされている Future 型は ListenableFuture
のみですが、カスタム Future 型を使用できます。Future を使用するには、サポートされている Future 型をクロス プロファイル メソッドの戻り値の型として宣言し、通常どおり使用します。
これはコールバックと同じ「異常な機能」があります。Future を返す同期メソッド(immediateFuture
を使用するなど)は、現在のプロファイルで実行する場合と別のプロファイルで実行する場合とで動作が異なります。SDK によって非同期とマークされたメソッドを使用する場合は、メソッドが呼び出されるタイミングを前提としない必要があります。
スレッド
メインスレッドでクロスプロファイル フューチャーまたはコールバックの結果をブロックしないでください。このようにすると、状況によってはコードが無期限にブロックされることがあります。これは、他のプロファイルへの接続もメインスレッドで確立されるためです。これは、クロス プロファイルの結果を待機中にブロックされている場合は発生しません。
対象
在庫状況リスナーを使用すると、在庫状況が変更されたときに通知を受け取ることができます。また、connector.utils().isAvailable
を使用すると、別のプロファイルを使用できるかどうかを判断できます。次に例を示します。
crossProfileConnector.registerAvailabilityListener(() -> {
if (crossProfileConnector.utils().isAvailable()) {
// Show cross-profile content
} else {
// Hide cross-profile content
}
});
接続ホルダー
接続ホルダーは、確立され維持されているクロス プロファイル接続に関心を持っていると記録される任意のオブジェクトです。
デフォルトでは、非同期呼び出しを行うと、呼び出しの開始時に接続ホルダーが追加され、結果またはエラーが発生すると削除されます。
接続ホルダーは手動で追加、削除して、接続をより細かく制御することもできます。接続ホルダーは connector.addConnectionHolder
を使用して追加し、connector.removeConnectionHolder
を使用して削除できます。
接続ホルダーが 1 つ以上追加されている場合、SDK は接続の維持を試みます。追加された接続ホルダーがない場合、接続を閉じることができます。
追加した接続ホルダーへの参照を維持し、関連性がなくなった場合は削除する必要があります。
同期呼び出し
同期呼び出しを行う前に、接続ホルダーを追加する必要があります。これは任意のオブジェクトを使用して行うことができますが、同期呼び出しの必要がなくなったときに削除できるように、そのオブジェクトを追跡する必要があります。
非同期呼び出し
非同期呼び出しを行うと、接続保持者が自動的に管理されるため、呼び出しと最初のレスポンスまたはエラーの間に接続が開きます。接続をこれ以上維持する必要がある場合(1 つのコールバックを使用して複数のレスポンスを受信する場合など)は、コールバック自体を接続ホルダーとして追加し、データを受信する必要がなくなったら削除する必要があります。
エラー処理
デフォルトでは、他のプロファイルが使用できないときに他のプロファイルを呼び出すと、UnavailableProfileException
がスローされます(または Future に渡されます。非同期呼び出しの場合はエラー コールバック)。
これを回避するには、#both()
または #suppliers()
を使用して、結果リスト内の任意の数のエントリの処理を行うコードを記述します(他のプロファイルが使用できない場合は 1、使用可能な場合は 2)。
例外
現在のプロファイルの呼び出し後に発生した未チェック例外は、通常どおり伝播されます。これは、呼び出しに使用したメソッド(#current()
、#personal
、#both
など)に関係なく適用されます。
他のプロファイルの呼び出し後に発生した未チェック例外は、元の例外が原因として ProfileRuntimeException
がスローされます。これは、呼び出しに使用したメソッド(#other()
、#personal
、#both
など)に関係なく適用されます。
ifAvailable
UnavailableProfileException
インスタンスをキャッチして処理する代わりに、.ifAvailable()
メソッドを使用して、UnavailableProfileException
をスローする代わりに返されるデフォルト値を指定できます。
次に例を示します。
profileNotesDatabase.other().ifAvailable().getNumberOfNotes(/* defaultValue= */ 0);
テスト
コードをテスト可能にするには、プロファイル コネクタを使用するコード(プロファイルの可用性を確認するコード、手動で接続するコードなど)にプロファイル コネクタのインスタンスを挿入する必要があります。また、プロファイル対応型のインスタンスが使用される場所にインスタンスを挿入する必要があります。
テストで使用できるコネクタと型のフェイクが用意されています。
まず、テストの依存関係を追加します。
testAnnotationProcessor
'com.google.android.enterprise.connectedapps:connectedapps-processor:1.1.2'
testCompileOnly
'com.google.android.enterprise.connectedapps:connectedapps-testing-annotations:1.1.2'
testImplementation
'com.google.android.enterprise.connectedapps:connectedapps-testing:1.1.2'
次に、テストクラスに @CrossProfileTest
アノテーションを付け、テスト対象の @CrossProfileConfiguration
アノテーション付きクラスを指定します。
@CrossProfileTest(configuration = MyApplication.class)
@RunWith(RobolectricTestRunner.class)
public class NotesMediatorTest {
}
これにより、構成で使用されるすべてのタイプとコネクタのフェイクが生成されます。
テストでこれらのフェイクのインスタンスを作成します。
private final FakeCrossProfileConnector connector = new
FakeCrossProfileConnector();
private final NotesManager personalNotesManager = new NotesManager(); //
real/mock/fake
private final NotesManager workNotesManager = new NotesManager(); // real/mock/fake
private final FakeProfileNotesManager profileNotesManager =
FakeProfileNotesManager.builder()
.personal(personalNotesManager)
.work(workNotesManager)
.connector(connector)
.build();
プロファイルの状態を設定します。
connector.setRunningOnProfile(PERSONAL);
connector.createWorkProfile();
connector.turnOffWorkProfile();
偽のコネクタとクロス プロファイル クラスをテスト対象のコードに渡して、呼び出しを行います。
呼び出しは正しいターゲットに転送され、切断されたプロファイルまたは使用できないプロファイルに呼び出しを行うと例外がスローされます。
対応種
次のタイプは、追加の作業なしでサポートされています。これらは、すべてのクロス プロファイル呼び出しの引数または戻り値の型として使用できます。
- プリミティブ(
byte
、short
、int
、long
、float
、double
、char
、boolean
) - ボックス化されたプリミティブ(
java.lang.Byte
、java.lang.Short
、java.lang.Integer
、java.lang.Long
、java.lang.Float
、java.lang.Double
、java.lang.Character
、java.lang.Boolean
、java.lang.Void
) java.lang.String
android.os.Parcelable
を実装するものjava.io.Serializable
を実装するもの- 単一ディメンションの非プリミティブ配列
java.util.Optional
java.util.Collection
java.util.List
java.util.Map
java.util.Set
android.util.Pair
com.google.common.collect.ImmutableMap
。
サポートされているジェネリック タイプ(java.util.Collection
など)には、サポートされている任意の型を型パラメータとして指定できます。次に例を示します。
java.util.Collection<java.util.Map<java.lang.String,MySerializableType[]>>
は有効な型です。
Futures
次の型は戻り値の型としてのみサポートされています。
com.google.common.util.concurrent.ListenableFuture
カスタム Parcelable ラッパー
型が上記のリストにない場合は、まず、android.os.Parcelable
または java.io.Serializable
を正しく実装できるかどうかを検討してください。見つからない場合は、Parcelable ラッパーを使用して、型のサポートを追加します。
カスタム Future ラッパー
上記のリストにない将来の型を使用する場合は、将来のラッパーを参照してサポートを追加してください。
Parcelable ラッパー
Parcelable ラッパーは、変更できない Parcelable 以外の型を SDK がサポートするための方法です。SDK には多くのタイプのラッパーが含まれていますが、使用する必要があるタイプが含まれていない場合は、独自のラッパーを作成する必要があります。
Parcelable Wrapper は、別のクラスをラップして Parcelable にするように設計されたクラスです。定義された静的コントラクトに従い、SDK に登録されているため、特定の型を Parcelable 型に変換したり、Parcelable 型からその型を抽出したりできます。
Annotation
Parcelable ラッパー クラスには @CustomParcelableWrapper
アノテーションを付け、ラップされたクラスを originalType
として指定する必要があります。次に例を示します。
@CustomParcelableWrapper(originalType=ImmutableList.class)
形式
Parcelable ラッパーは Parcelable
を正しく実装し、ラップされた型をラップする静的 W of(Bundler, BundlerType, T)
メソッドと、ラップされた型を返す非静的 T get()
メソッドが必要です。
SDK はこれらのメソッドを使用して、型をシームレスにサポートします。
バンドラ
汎用型(リストやマップなど)をラップできるように、of
メソッドには、サポートされているすべての型を Parcel
に読み取り(#readFromParcel
を使用)および書き込み(#writeToParcel
を使用)できる Bundler
と、書き込まれる宣言型を表す BundlerType
が渡されます。
Bundler
インスタンスと BundlerType
インスタンス自体は Parcelable であり、Parcelable ラッパーの Parcel 化の一部として書き込まれるため、Parcelable ラッパーの再構築時に使用できます。
BundlerType
が汎用型を表す場合、型変数は .typeArguments()
を呼び出すことで見つけることができます。各型引数自体が BundlerType
です。
例として、ParcelableCustomWrapper
をご覧ください。
public class CustomWrapper<F> {
private final F value;
public CustomWrapper(F value) {
this.value = value;
}
public F value() {
return value;
}
}
@CustomParcelableWrapper(originalType = CustomWrapper.class)
public class ParcelableCustomWrapper<E> implements Parcelable {
private static final int NULL = -1;
private static final int NOT_NULL = 1;
private final Bundler bundler;
private final BundlerType type;
private final CustomWrapper<E> customWrapper;
/**
* Create a wrapper for a given {@link CustomWrapper}.
*
* <p>The passed in {@link Bundler} must be capable of bundling {@code F}.
*/
public static <F> ParcelableCustomWrapper<F> of(
Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {
return new ParcelableCustomWrapper<>(bundler, type, customWrapper);
}
public CustomWrapper<E> get() {
return customWrapper;
}
private ParcelableCustomWrapper(
Bundler bundler, BundlerType type, CustomWrapper<E> customWrapper) {
if (bundler == null || type == null) {
throw new NullPointerException();
}
this.bundler = bundler;
this.type = type;
this.customWrapper = customWrapper;
}
private ParcelableCustomWrapper(Parcel in) {
bundler = in.readParcelable(Bundler.class.getClassLoader());
int presentValue = in.readInt();
if (presentValue == NULL) {
type = null;
customWrapper = null;
return;
}
type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());
BundlerType valueType = type.typeArguments().get(0);
@SuppressWarnings("unchecked")
E value = (E) bundler.readFromParcel(in, valueType);
customWrapper = new CustomWrapper<>(value);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(bundler, flags);
if (customWrapper == null) {
dest.writeInt(NULL);
return;
}
dest.writeInt(NOT_NULL);
dest.writeParcelable(type, flags);
BundlerType valueType = type.typeArguments().get(0);
bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);
}
@Override
public int describeContents() {
return 0;
}
@SuppressWarnings("rawtypes")
public static final Creator<ParcelableCustomWrapper> CREATOR =
new Creator<ParcelableCustomWrapper>() {
@Override
public ParcelableCustomWrapper createFromParcel(Parcel in) {
return new ParcelableCustomWrapper(in);
}
@Override
public ParcelableCustomWrapper[] newArray(int size) {
return new ParcelableCustomWrapper[size];
}
};
}
SDK に登録する
作成したカスタム Parcelable ラッパーを使用するには、SDK に登録する必要があります。
これを行うには、CustomProfileConnector
アノテーションまたはクラスの CrossProfile
アノテーションで parcelableWrappers={YourParcelableWrapper.class}
を指定します。
Future Wrapper
フューチャー ラッパーは、SDK がプロファイル全体でフューチャーをサポートする方法です。SDK にはデフォルトで ListenableFuture
のサポートが含まれていますが、他の Future 型の場合は、自分でサポートを追加できます。
Future ラッパーは、特定の Future 型をラップして SDK で使用できるように設計されたクラスです。定義された静的コントラクトに従い、SDK に登録する必要があります。
Annotation
将来のラッパー クラスには @CustomFutureWrapper
アノテーションを付け、ラップされたクラスを originalType
として指定する必要があります。次に例を示します。
@CustomFutureWrapper(originalType=SettableFuture.class)
形式
今後のラッパーは com.google.android.enterprise.connectedapps.FutureWrapper
を拡張する必要があります。
今後のラッパーには、ラッパーのインスタンスを作成する静的 W create(Bundler, BundlerType)
メソッドが必要です。同時に、ラップされた Future 型のインスタンスが作成されます。これは、非静的 T
getFuture()
メソッドによって返される必要があります。結果またはスロー可能オブジェクトをラップされた Future に渡すように、onResult(E)
メソッドと onException(Throwable)
メソッドを実装する必要があります。
将来のラッパーには、静的な void writeFutureResult(Bundler,
BundlerType, T, FutureResultWriter<E>)
メソッドも必要です。これは、結果が渡されたときに登録され、結果が渡されたときに resultWriter.onSuccess(value)
を呼び出します。例外が指定されている場合は、resultWriter.onFailure(exception)
を呼び出す必要があります。
最後に、フューチャー ラッパーには、プロファイルからフューチャーへのマップを、プロファイルから結果へのマップのフューチャーに変換する静的 T<Map<Profile, E>>
groupResults(Map<Profile, T<E>> results)
メソッドも必要です。CrossProfileCallbackMultiMerger
を使用すると、このロジックを簡単にできます。
次に例を示します。
/** A basic implementation of the future pattern used to test custom future
wrappers. */
public class SimpleFuture<E> {
public static interface Consumer<E> {
void accept(E value);
}
private E value;
private Throwable thrown;
private final CountDownLatch countDownLatch = new CountDownLatch(1);
private Consumer<E> callback;
private Consumer<Throwable> exceptionCallback;
public void set(E value) {
this.value = value;
countDownLatch.countDown();
if (callback != null) {
callback.accept(value);
}
}
public void setException(Throwable t) {
this.thrown = t;
countDownLatch.countDown();
if (exceptionCallback != null) {
exceptionCallback.accept(thrown);
}
}
public E get() {
try {
countDownLatch.await();
} catch (InterruptedException e) {
eturn null;
}
if (thrown != null) {
throw new RuntimeException(thrown);
}
return value;
}
public void setCallback(Consumer<E> callback, Consumer<Throwable>
exceptionCallback) {
if (value != null) {
callback.accept(value);
} else if (thrown != null) {
exceptionCallback.accept(thrown);
} else {
this.callback = callback;
this.exceptionCallback = exceptionCallback;
}
}
}
/** Wrapper for adding support for {@link SimpleFuture} to the Connected Apps SDK.
*/
@CustomFutureWrapper(originalType = SimpleFuture.class)
public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {
private final SimpleFuture<E> future = new SimpleFuture<>();
public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType
bundlerType) {
return new SimpleFutureWrapper<>(bundler, bundlerType);
}
private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {
super(bundler, bundlerType);
}
public SimpleFuture<E> getFuture() {
return future;
}
@Override
public void onResult(E result) {
future.set(result);
}
@Override
public void onException(Throwable throwable) {
future.setException(throwable);
}
public static <E> void writeFutureResult(
SimpleFuture<E> future, FutureResultWriter<E> resultWriter) {
future.setCallback(resultWriter::onSuccess, resultWriter::onFailure);
}
public static <E> SimpleFuture<Map<Profile, E>> groupResults(
Map<Profile, SimpleFuture<E>> results) {
SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();
CrossProfileCallbackMultiMerger<E> merger =
new CrossProfileCallbackMultiMerger<>(results.size(), m::set);
for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {
result
.getValue()
.setCallback(
(value) -> merger.onResult(result.getKey(), value),
(throwable) -> merger.missingResult(result.getKey()));
}
return m;
}
}
SDK に登録する
作成したカスタム フューチャー ラッパーを使用するには、SDK に登録する必要があります。
そのためには、CustomProfileConnector
アノテーションまたはクラスの CrossProfile
アノテーションで futureWrappers={YourFutureWrapper.class}
を指定します。
ダイレクト ブート モード
アプリがダイレクト ブート モードをサポートしている場合は、プロファイルのロック解除前にクロス プロファイル コールを行う必要がある場合があります。デフォルトでは、SDK は他のプロファイルがロック解除されている場合にのみ接続を許可します。
この動作を変更するには、カスタム プロファイル コネクタを使用している場合は availabilityRestrictions=AvailabilityRestrictions.DIRECT_BOOT_AWARE
を指定する必要があります。
@GeneratedProfileConnector
@CustomProfileConnector(availabilityRestrictions=AvailabilityRestrictions.DIRECT_BO
OT_AWARE)
public interface MyProfileConnector extends ProfileConnector {
public static MyProfileConnector create(Context context) {
return GeneratedMyProfileConnector.builder(context).build();
}
}
CrossProfileConnector
を使用している場合は、ビルダーで .setAvailabilityRestrictions(AvailabilityRestrictions.DIRECT_BOOT
_AWARE
を使用します。
この変更により、他のプロフィールがロック解除されていない場合でも、利用可能かどうかが通知され、クロス プロファイル通話が可能になります。通話がデバイスの暗号化ストレージにのみアクセスするようにする責任はお客様にあります。