這些章節僅供參考,您不必從上到下閱讀。
要求使用者同意
使用架構 API:
CrossProfileApps.canInteractAcrossProfiles()
CrossProfileApps.canRequestInteractAcrossProfiles()
CrossProfileApps.createRequestInteractAcrossProfilesIntent()
CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED
這些 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
。
裝置政策控制器
如果您的應用程式是裝置政策控制器,則必須指定參照 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
) 或接受回呼參數的方法,都會標示為非阻斷。所有其他方法都會標示為阻斷。
建議使用非同步呼叫。如果您必須使用同步呼叫,請參閱「同步呼叫」。
回呼
最基本的非阻斷式呼叫類型是 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();
將假的連接器和跨設定檔類別傳遞至測試中的程式碼,然後進行呼叫。
呼叫會導向正確的目標,且在呼叫已中斷連線或無法使用的設定檔時,系統會擲回例外狀況。
適用類型
您不必額外採取任何行動,即可使用下列類型。這些類型可用於所有跨設定檔呼叫的引數或傳回類型。
- 基本元素 (
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
。如果無法看到可分割的包裝函式,系統就無法為您的類型新增支援。
自訂 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
,代表要寫入的已宣告類型。
Bundler
和 BundlerType
例項本身可分割,因此應寫入分割可分割包裝函式的一部分,以便在重建可分割包裝函式時使用。
如果 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
。
這項變更可讓您在未解鎖其他設定檔時,瞭解可用性並進行跨設定檔的通話。您有責任確保您的呼叫只會存取裝置加密儲存空間。