進階主題

這些章節僅供參考,您不必從上到下閱讀。

使用架構 API:

這些 API 會在 SDK 中包裝,以便提供更一致的 API 途徑 (例如避免使用 UserHandle 物件),但目前您可以直接呼叫這些 API。

實作方式很簡單:如果可以互動,就繼續進行。如果沒有,但您可以提出要求,請向使用者顯示提示/橫幅/工具提示等。如果使用者同意前往「設定」,請建立要求意圖,並使用 Context#startActivity 將使用者帶往該頁面。您可以使用廣播訊息偵測這項功能何時變更,也可以在使用者返回時再次檢查。

如要測試這項功能,您必須在工作資料夾中開啟 TestDPC,然後前往最底部,並選取將套件名稱加入已連結應用程式許可清單。這會模擬管理員為應用程式建立「許可清單」。

詞彙解釋

本節定義與跨設定開發相關的重要術語。

跨設定檔設定

跨設定檔設定會將相關的跨設定檔提供者類別分組,並為跨設定檔功能提供一般設定。通常每個程式碼集都會有一個 @CrossProfileConfiguration 註解,但在某些複雜的應用程式中,可能會有多個註解。

個人資料連接器

連接器會管理設定檔之間的連結。通常,每個交叉設定檔類型都會指向特定連接器。單一設定中的每個跨資料類型都必須使用相同的連接器。

跨設定檔提供者類別

跨設定檔提供者類別會將相關的跨設定檔類型分組。

Mediator

中介者位於高階和低階程式碼之間,將呼叫分發至正確的設定檔,並合併結果。這是唯一需要瞭解設定檔的程式碼。這是一種架構概念,而非內建於 SDK 中的內容。

跨設定檔類型

跨設定檔類型是包含 @CrossProfile 註解方法的類別或介面。這類型中的程式碼不需要瞭解設定檔,且最好只針對本機資料執行。

設定檔類型

設定檔類型
目前我們要執行的有效設定檔。
其他(如果有) 我們未執行的設定檔。
個人使用者 0,裝置上無法關閉的設定檔。
公司通常是使用者 10 (但可能會更高),可切換開啟和關閉,用於容納工作應用程式和資料。
主要可由應用程式自行定義。顯示兩個設定檔合併檢視畫面的設定檔。
次要如果已定義主要設定檔,次要設定檔就是非主要設定檔。
供應商主要設定檔的供應者是兩個設定檔,而次要設定檔的供應者則只有次要設定檔本身。

商家檔案 ID

代表資料夾類型的類別 (個人或工作)。這些值會由在多個設定檔中執行的方法傳回,並可用於在這些設定檔中執行更多程式碼。這些資料可以序列化為 int,方便儲存。

本指南將概述在 Android 應用程式中建構高效且可維護的跨設定檔功能的建議結構。

CrossProfileConnector 轉換為單例

在應用程式的整個生命週期中,應只使用單一例項,否則會建立平行連線。您可以使用 Dagger 等依附元件插入架構,也可以在新的或現有的類別中使用經典的單例模式來執行此操作。

在您發出呼叫時,將產生的 Profile 例項插入或傳入至您的類別,而非在方法中建立

這樣一來,您之後就能在單元測試中傳入自動產生的 FakeProfile 例項。

考慮使用中介模式

這個常見的模式是讓其中一個現有 API (例如 getEvents()) 為所有呼叫端提供設定檔相關資訊。在這種情況下,現有的 API 可以成為「仲介」方法或類別,其中包含對產生的跨設定檔程式碼的新呼叫。

這樣一來,您就不會強制要求每個呼叫端都知道如何進行跨設定檔呼叫,而是讓這項功能成為 API 的一部分。

請考慮是否要將介面方法標註為 @CrossProfile,以免在供應器中公開實作類別

這項功能與依附元件插入架構搭配得很好。

如果您從跨資料夾呼叫收到任何資料,請考慮是否要新增參照資料夾來源的欄位

這是不錯的做法,因為您可能會想在 UI 層級瞭解這項資訊 (例如在工作內容中新增徽章圖示)。如果沒有此屬性,任何資料 ID (例如套件名稱) 都可能不再是唯一值,因此也可能需要使用此屬性。

跨設定檔

本節將概略說明如何建構自己的跨設定檔互動。

主要設定檔

本文件範例中的大部分呼叫都包含明確的操作說明,說明要在哪個設定檔上執行,包括工作、個人或兩者皆是。

實際上,如果應用程式只在單一設定檔中提供合併的體驗,您可能會希望這項決定取決於您執行的設定檔,因此也有類似的方便方法可考量這點,以免程式碼庫充斥 if-else 設定檔條件。

建立連接器執行個體時,您可以指定哪個設定檔類型是「主要」(例如「工作」)。這可啟用其他選項,例如:

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,表示您指出此方法的某些實作項目應可跨設定檔存取。

您可以在 Cross Profile Provider 中傳回任何 Cross Profile 介面的實作項目,這樣一來,您就會表示此實作項目應可跨設定檔存取。您不需要為實作類別加上註解。

跨設定檔供應器

每個跨設定檔類型都必須由標示為 @CrossProfileProvider 的方法提供。每當跨設定檔呼叫時,系統都會呼叫這些方法,因此建議您為每個類型維護單例。

建構函式

供應器必須有公用建構函式,該函式不帶引數或只帶一個 Context 引數。

供應者方法

供應器方法必須不使用引數,或使用單一 Context 引數。

依附元件插入

如果您使用 Dagger 等依附元件插入架構來管理依附元件,建議您讓該架構按照平常方式建立跨設定檔類型,然後將這些類型插入提供者類別。這樣一來,@CrossProfileProvider 方法就能傳回這些插入的例項。

個人資料連接器

每個跨設定檔設定都必須有單一設定檔連接器,負責管理與其他設定檔的連線。

預設設定檔連接器

如果程式碼集只有一個跨設定檔設定,您可以避免自行建立設定檔連接器,並使用 com.google.android.enterprise.connectedapps.CrossProfileConnector。如果未指定,則會使用這個預設值。

建構跨設定檔連接器時,您可以在建構工具上指定一些選項:

  • 排程執行緒服務

    如要控管 SDK 建立的執行緒,請使用 #setScheduledExecutorService()

  • 繫結機制

    如果您有特定的設定檔繫結需求,請使用 #setBinder。這項屬性可能只會由裝置政策控制器使用。

自訂設定檔連接器

您需要自訂設定檔連接器才能設定某些設定 (使用 CustomProfileConnector),如果您需要在單一程式碼庫中使用多個連接器,也需要使用自訂設定檔連接器 (例如,如果您有多個程序,建議每個程序各使用一個連接器)。

建立 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

裝置政策控制器

如果您的應用程式是裝置政策控制器,則必須指定參照 DeviceAdminReceiverDpcProfileBinder 例項。

如果您要實作自己的設定檔連接器:

@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) 或接受回呼參數的方法,都會標示為非阻斷。所有其他方法都會標示為阻斷。

建議使用非同步呼叫。如果您必須使用同步呼叫,請參閱「同步呼叫」。

回呼

最基本的非阻斷式呼叫類型是 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
});

如果這個介面包含單一方法,且該方法只接受零個或一個參數,則也可用於一次呼叫多個設定檔。

您可以使用回呼傳遞任意數量的值,但連線只會保留給第一個值。如要瞭解如何保持連線狀態以接收更多值,請參閱「連線持有者」。

含回呼的同步方法

使用 SDK 的回呼功能有一個不尋常的特色,就是您可以技術上撰寫使用回呼的同步方法:

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 標示為非同步方法的情況,都不得對方法的呼叫時間做出任何假設。

簡易回呼

「簡易回呼」是一種較嚴格的回呼形式,可在進行跨設定檔呼叫時提供額外功能。簡單介面必須包含單一方法,可接受零個或一個參數。

您可以在 @CrossProfileCallback 註解中指定 simple=true,強制要求回呼介面必須保留。

簡單的回呼可搭配 .both().suppliers() 等各種方法使用。

連線持有者

在進行非同步呼叫 (使用回呼或 Future) 時,系統會在呼叫時新增連線持有者,並在傳遞例外狀況或值時移除。

如果您希望使用回呼傳遞多個結果,請手動將回呼設為連線持有者:

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
}

如果我們使用回呼或 Future 進行呼叫,系統會保持連線,直到傳遞結果為止。如果我們判定不會傳遞結果,則應移除回呼或未來做為連線持有者:

connector.removeConnectionHolder(myCallback);
connector.removeConnectionHolder(future);

詳情請參閱「連線持有者」。

Futures

SDK 也原生支援 Future。唯一原生支援的 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 移除。

至少新增一個連線持有者時,SDK 會嘗試維持連線。新增的連線持有者為零時,即可關閉連線。

您必須保留所新增連線持有者的參照,並在不再相關時予以移除。

同步呼叫

在進行同步呼叫之前,應先新增連線持有者。您可以使用任何物件執行這項操作,但必須追蹤該物件,以便在不再需要進行同步呼叫時將其移除。

非同步呼叫

在進行非同步呼叫時,系統會自動管理連線持有者,以便在呼叫和第一個回應或錯誤之間建立連線。如果您需要在這個時間點之後保留連線 (例如使用單一回呼接收多個回應),請將回呼本身新增為連線持有者,並在不再需要接收其他資料時將其移除。

處理錯誤

根據預設,如果在其他設定檔無法使用時對其發出呼叫,就會擲回 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();

將假的連接器和跨設定檔類別傳遞至測試中的程式碼,然後進行呼叫。

呼叫會導向正確的目標,且在呼叫已中斷連線或無法使用的設定檔時,系統會擲回例外狀況。

適用類型

您不必額外採取任何行動,即可使用下列類型。這些類型可用於所有跨設定檔呼叫的引數或傳回類型。

  • 基本元素 (byteshortintlongfloatdoublecharboolean)
  • 封箱原始值 (java.lang.Bytejava.lang.Shortjava.lang.Integerjava.lang.Longjava.lang.Floatjava.lang.Doublejava.lang.Characterjava.lang.Booleanjava.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.Parcelablejava.io.Serializable。如果無法看到可分割的包裝函式,系統就無法為您的類型新增支援。

自訂 Future 包裝函式

如果您想使用上述清單中未列出的未來型別,請參閱未來包裝函式,瞭解如何新增支援。

Parcelable 包裝函式

可分割包裝函式是 SDK 新增支援不可修改的可分割類型的方式。SDK 包含許多類型的包裝函式,但如果您需要使用的類型不在其中,就必須自行撰寫。

Parcelable Wrapper 是用來包裝其他類別並使其可分割的類別。它遵循已定義的靜態合約,並已在 SDK 中註冊,因此可用於將指定類型轉換為可分割類型,並從可分割類型中擷取該類型。

註解

可包裝的包裝函式類別必須加上 @CustomParcelableWrapper 註解,並將包裝的類別指定為 originalType。例如:

@CustomParcelableWrapper(originalType=ImmutableList.class)

格式

Parcelable 包裝函式必須正確實作 Parcelable,且必須具有包裝函式類型和傳回包裝函式類型的靜態 W of(Bundler, BundlerType, T) 方法。T get()

SDK 會使用這些方法,為類型提供無縫支援。

Bundler

為了包裝泛型類型 (例如清單和地圖),of 方法會傳遞 Bundler,後者可讀取 (使用 #readFromParcel) 並寫入 (使用 #writeToParcel) 所有支援的類型至 Parcel,以及 BundlerType,代表要寫入的已宣告類型。

BundlerBundlerType 例項本身可分割,因此應寫入分割可分割包裝函式的一部分,以便在重建可分割包裝函式時使用。

如果 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 註冊

建立自訂可分割包裝函式後,您必須將其註冊至 SDK,才能使用該函式。

如要執行這項操作,請在類別的 CustomProfileConnector 註解或 CrossProfile 註解中指定 parcelableWrappers={YourParcelableWrapper.class}

Future 包裝函式

未來包裝函式是 SDK 在各設定檔中新增對未來的支援方式。SDK 預設支援 ListenableFuture,但您可以自行為其他 Future 類型新增支援。

Future 包裝函式是一種類別,旨在包裝特定的 Future 類型,並讓 SDK 使用該類型。它遵循已定義的靜態合約,且必須向 SDK 註冊。

註解

未來的包裝函式類別必須加上 @CustomFutureWrapper 註解,並將包裝的類別指定為 originalType。例如:

@CustomFutureWrapper(originalType=SettableFuture.class)

格式

Future 包裝函式必須擴充 com.google.android.enterprise.connectedapps.FutureWrapper

未來的包裝函式必須具有靜態 W create(Bundler, BundlerType) 方法,用於建立包裝函式的例項。同時,這應該會建立包裝的 Future 類型例項。這應該由非靜態 T getFuture() 方法傳回。必須實作 onResult(E)onException(Throwable) 方法,才能將結果或可擲回項目傳遞至包裝的 Future。

Future 包裝函式也必須具有靜態 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

這項變更可讓您在未解鎖其他設定檔時,瞭解可用性並進行跨設定檔的通話。您有責任確保您的呼叫只會存取裝置加密儲存空間。