Bagian ini dimaksudkan sebagai referensi dan Anda tidak perlu membacanya dari atas ke bawah.
Meminta persetujuan pengguna
Menggunakan API framework:
CrossProfileApps.canInteractAcrossProfiles()
CrossProfileApps.canRequestInteractAcrossProfiles()
CrossProfileApps.createRequestInteractAcrossProfilesIntent()
CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED
API ini akan digabungkan dalam SDK untuk platform API yang lebih konsisten (misalnya, menghindari objek UserHandle), tetapi untuk saat ini, Anda dapat memanggilnya secara langsung.
Implementasinya sederhana: jika Anda dapat berinteraksi, lanjutkan. Jika tidak,
tetapi Anda dapat memintanya, tampilkan perintah/banner/tooltip/dll. pengguna. Jika pengguna
setuju untuk membuka Setelan, buat intent permintaan dan gunakan
Context#startActivity
untuk mengirim pengguna ke sana. Anda dapat menggunakan siaran
untuk mendeteksi kapan kemampuan ini berubah, atau cukup periksa lagi saat pengguna
kembali.
Untuk mengujinya, Anda harus membuka TestDPC di profil kerja, membuka bagian paling bawah, lalu memilih untuk menambahkan nama paket ke daftar yang diizinkan aplikasi yang terhubung. Tindakan ini meniru 'izinkan' admin untuk aplikasi Anda.
Glosarium
Bagian ini menjelaskan istilah utama yang terkait dengan pengembangan lintas profil.
Konfigurasi Antar-Profil
Konfigurasi Antar-Profil mengelompokkan Class Penyedia Antar-Profil
terkait dan memberikan konfigurasi umum untuk fitur antar-profil.
Biasanya akan ada satu anotasi @CrossProfileConfiguration
per codebase, tetapi dalam beberapa aplikasi kompleks mungkin ada beberapa anotasi.
Konektor Profil
Konektor mengelola koneksi antar-profil. Biasanya, setiap jenis lintas profil akan mengarah ke Konektor tertentu. Setiap jenis lintas profil dalam satu konfigurasi harus menggunakan Konektor yang sama.
Class Penyedia Antar-Profil
Class Penyedia Lintas Profil mengelompokkan Jenis Lintas Profil terkait.
Mediator
Mediator berada di antara kode tingkat tinggi dan tingkat rendah, mendistribusikan panggilan ke profil yang benar dan menggabungkan hasil. Ini adalah satu-satunya kode yang perlu memiliki profil. Ini adalah konsep arsitektur, bukan sesuatu yang di-build ke dalam SDK.
Jenis Antar-Profil
Jenis lintas profil adalah class atau antarmuka yang berisi metode yang dianotasi
@CrossProfile
. Kode dalam jenis ini tidak perlu mengetahui profil dan sebaiknya
hanya bertindak berdasarkan data lokalnya.
Jenis Profil
Jenis Profil | |
---|---|
Saat ini | Profil aktif tempat kita menjalankan. |
Lainnya | (jika ada) Profil tempat kita tidak menjalankan. |
Pribadi | Pengguna 0, profil di perangkat yang tidak dapat dinonaktifkan. |
Kantor | Biasanya pengguna 10, tetapi mungkin lebih tinggi, dapat diaktifkan dan dinonaktifkan, digunakan untuk menyimpan aplikasi dan data kerja. |
Utama | Dapat ditentukan secara opsional oleh aplikasi. Profil yang menampilkan tampilan gabungan dari kedua profil. |
Sekunder | Jika primer ditentukan, sekunder adalah profil yang bukan primer. |
Pemasok | Pemasok untuk profil utama adalah kedua profil, pemasok untuk profil sekunder hanya profil sekunder itu sendiri. |
ID Profil
Class yang mewakili jenis profil (pribadi atau kerja). Ini akan
ditampilkan oleh metode yang berjalan di beberapa profil dan dapat digunakan untuk menjalankan lebih banyak
kode di profil tersebut. Ini dapat diserialisasi ke int
untuk penyimpanan
yang praktis.
Solusi yang direkomendasikan arsitektur
Panduan ini menguraikan struktur yang direkomendasikan untuk membuat fungsi lintas profil yang efisien dan dapat dikelola dalam aplikasi Android Anda.
Mengonversi CrossProfileConnector
menjadi singleton
Hanya satu instance yang boleh digunakan selama siklus proses aplikasi, atau Anda akan membuat koneksi paralel. Hal ini dapat dilakukan menggunakan framework injeksi dependensi seperti Dagger, atau dengan menggunakan pola Singleton klasik, baik di class baru maupun yang sudah ada.
Masukkan atau teruskan instance Profil yang dihasilkan ke dalam class Anda saat Anda melakukan panggilan, bukan membuatnya dalam metode
Hal ini memungkinkan Anda meneruskan instance FakeProfile
yang dibuat secara otomatis dalam
pengujian unit nanti.
Pertimbangkan pola mediator
Pola umum ini adalah membuat salah satu API yang ada (misalnya getEvents()
)
memiliki profil untuk semua pemanggilnya. Dalam hal ini, API yang ada hanya dapat
menjadi metode atau class 'mediator' yang berisi panggilan baru ke kode lintas profil
yang dihasilkan.
Dengan cara ini, Anda tidak memaksa setiap pemanggil untuk mengetahui cara melakukan panggilan lintas profil, tetapi panggilan tersebut hanya menjadi bagian dari API Anda.
Pertimbangkan apakah akan menganotasi metode antarmuka sebagai @CrossProfile
untuk menghindari keharusan mengekspos class implementasi Anda di penyedia
Hal ini berfungsi dengan baik dengan framework injeksi dependensi.
Jika Anda menerima data dari panggilan lintas profil, pertimbangkan apakah akan menambahkan kolom yang mereferensikan profil asalnya
Ini bisa menjadi praktik yang baik karena Anda mungkin ingin mengetahuinya di lapisan UI (misalnya, menambahkan ikon badge ke item kerja). Hal ini juga mungkin diperlukan jika ID data tidak lagi unik tanpanya, seperti nama paket.
Antar-Profil
Bagian ini menguraikan cara mem-build interaksi Lintas Profil Anda sendiri.
Profil Utama
Sebagian besar panggilan dalam contoh pada dokumen ini berisi petunjuk eksplisit tentang profil yang akan dijalankan, termasuk kerja, pribadi, dan keduanya.
Dalam praktiknya, untuk aplikasi dengan pengalaman gabungan di satu profil saja, Anda mungkin ingin keputusan ini bergantung pada profil yang Anda jalankan, sehingga ada metode praktis serupa yang juga mempertimbangkan hal ini, untuk menghindari codebase Anda yang dipenuhi dengan kondisional profil if-else.
Saat membuat instance konektor, Anda dapat menentukan jenis profil mana yang 'utama' (misalnya, 'KERJA'). Tindakan ini memungkinkan opsi tambahan, seperti berikut:
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();
Jenis Antar-Profil
Class dan antarmuka yang berisi metode yang dianotasi @CrossProfile
disebut sebagai Jenis Lintas Profil.
Implementasi Jenis Lintas Profil harus tidak bergantung pada profil, yaitu profil tempatnya berjalan. Metode ini diizinkan untuk melakukan panggilan ke metode lain dan secara umum harus berfungsi seperti sedang berjalan di satu profil. Mereka hanya akan memiliki akses ke status di profil mereka sendiri.
Contoh Jenis Antar-Profil:
public class Calculator {
@CrossProfile
public int add(int a, int b) {
return a + b;
}
}
Anotasi class
Untuk menyediakan API terkuat, Anda harus menentukan konektor untuk setiap jenis lintas profil, seperti:
@CrossProfile(connector=MyProfileConnector.class)
public class Calculator {
@CrossProfile
public int add(int a, int b) {
return a + b;
}
}
Hal ini bersifat opsional, tetapi berarti API yang dihasilkan akan lebih spesifik dalam jenis dan lebih ketat dalam pemeriksaan waktu kompilasi.
Antarmuka
Dengan menganotasi metode di antarmuka sebagai @CrossProfile
, Anda menyatakan bahwa
mungkin ada beberapa implementasi metode ini yang harus dapat diakses
di seluruh profil.
Anda dapat menampilkan implementasi antarmuka Lintas Profil apa pun di Penyedia Lintas Profil dan dengan melakukannya, Anda menyatakan bahwa implementasi ini harus dapat diakses lintas profil. Anda tidak perlu menganotasi class implementasi.
Penyedia Antar-Profil
Setiap Jenis Lintas Profil harus disediakan oleh metode yang dianotasi
@CrossProfileProvider
. Metode ini akan dipanggil setiap kali panggilan lintas profil
dilakukan, jadi sebaiknya Anda mempertahankan singleton untuk setiap jenis.
Konstruktor
Penyedia harus memiliki konstruktor publik yang tidak menggunakan argumen atau
satu argumen Context
.
Metode Penyedia
Metode penyedia tidak boleh mengambil argumen atau satu argumen Context
.
Injeksi Dependensi
Jika Anda menggunakan framework injeksi dependensi seperti Dagger untuk mengelola
dependensi, sebaiknya Anda membuat framework tersebut membuat jenis
lintas profil seperti biasa, lalu memasukkan jenis tersebut ke dalam
class penyedia. Metode @CrossProfileProvider
kemudian dapat menampilkan instance
yang dimasukkan tersebut.
Konektor Profil
Setiap Konfigurasi Lintas Profil harus memiliki satu Konektor Profil, yang bertanggung jawab untuk mengelola koneksi ke profil lain.
Konektor Profil Default
Jika hanya ada satu Konfigurasi Lintas Profil dalam codebase, Anda dapat
menghindari pembuatan Konektor Profil Anda sendiri dan menggunakan
com.google.android.enterprise.connectedapps.CrossProfileConnector
. Ini adalah
default yang digunakan jika tidak ada yang ditentukan.
Saat membuat Konektor Lintas Profil, Anda dapat menentukan beberapa opsi di builder:
Layanan Eksekutor Terjadwal
Jika Anda ingin memiliki kontrol atas thread yang dibuat oleh SDK, gunakan
#setScheduledExecutorService()
,Binder
Jika Anda memiliki kebutuhan khusus terkait binding profil, gunakan
#setBinder
. Hal ini mungkin hanya digunakan oleh Pengontrol Kebijakan Perangkat.
Konektor Profil Kustom
Anda memerlukan konektor profil kustom agar dapat menetapkan beberapa konfigurasi (menggunakan CustomProfileConnector
) dan akan memerlukannya jika memerlukan beberapa konektor dalam satu codebase (misalnya, jika Anda memiliki beberapa proses, sebaiknya gunakan satu konektor per proses).
Saat membuat ProfileConnector
, tampilannya akan terlihat seperti:
@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
Untuk mengubah nama layanan yang dihasilkan (yang harus dirujuk dalam
AndroidManifest.xml
Anda), gunakanserviceClassName=
.primaryProfile
Untuk menentukan profil utama, gunakan
primaryProfile
.availabilityRestrictions
Untuk mengubah batasan yang diterapkan SDK pada koneksi dan ketersediaan profil, gunakan
availabilityRestrictions
.
Pengontrol Kebijakan Perangkat
Jika aplikasi Anda adalah Pengontrol Kebijakan Perangkat, Anda harus menentukan instance
DpcProfileBinder
yang mereferensikan DeviceAdminReceiver
.
Jika Anda menerapkan konektor profil Anda sendiri:
@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();
}
}
atau menggunakan CrossProfileConnector
default:
CrossProfileConnector connector =
CrossProfileConnector.builder(context).setBinder(new DpcProfileBinder(new
ComponentName("com.google.testdpc", "AdminReceiver"))).build();
Konfigurasi Antar-Profil
Anotasi @CrossProfileConfiguration
digunakan untuk menautkan semua jenis
lintas profil menggunakan konektor untuk mengirim panggilan metode dengan benar. Untuk
melakukannya, kita menganotasi class dengan @CrossProfileConfiguration
yang mengarah ke
setiap penyedia, seperti ini:
@CrossProfileConfiguration(providers = {TestProvider.class})
public abstract class TestApplication {
}
Tindakan ini akan memvalidasi bahwa untuk semua Jenis Profil Lintas, konektor profilnya sama atau tidak ada konektor yang ditentukan.
serviceSuperclass
Secara default, layanan yang dihasilkan akan menggunakan
android.app.Service
sebagai superclass. Jika Anda memerlukan class yang berbeda (yang harus berupa subclassandroid.app.Service
) sebagai superclass, tentukanserviceSuperclass=
.serviceClass
Jika ditentukan, tidak ada layanan yang akan dihasilkan. Ini harus cocok dengan
serviceClassName
di konektor profil yang Anda gunakan. Layanan kustom Anda harus mengirim panggilan menggunakan class_Dispatcher
yang dihasilkan seperti:
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;
}
}
Ini dapat digunakan jika Anda perlu melakukan tindakan tambahan sebelum atau setelah panggilan lintas profil.
Konektor
Jika menggunakan konektor selain
CrossProfileConnector
default, Anda harus menentukannya menggunakanconnector=
.
Visibilitas
Setiap bagian aplikasi Anda yang berinteraksi lintas profil harus dapat melihat Konektor Profil Anda.
Class yang dianotasi @CrossProfileConfiguration
harus dapat melihat setiap
penyedia yang digunakan dalam aplikasi Anda.
Panggilan Sinkron
Connected Apps SDK mendukung panggilan sinkron (pemblokiran) untuk kasus saat panggilan tersebut tidak dapat dihindari. Namun, ada sejumlah kelemahan dalam menggunakan panggilan ini (seperti potensi panggilan untuk diblokir dalam waktu lama), jadi sebaiknya Anda menghindari panggilan sinkron jika memungkinkan. Untuk menggunakan panggilan asinkron, lihat Panggilan asinkron .
Holder Koneksi
Jika menggunakan panggilan sinkron, Anda harus memastikan bahwa ada holder koneksi yang terdaftar sebelum melakukan panggilan lintas profil. Jika tidak, pengecualian akan ditampilkan. Untuk informasi selengkapnya, lihat Holder Koneksi.
Untuk menambahkan holder koneksi, panggil ProfileConnector#addConnectionHolder(Object)
dengan objek apa pun (kemungkinan, instance objek yang melakukan
panggilan lintas profil). Tindakan ini akan mencatat bahwa objek ini menggunakan koneksi dan akan mencoba membuat koneksi. Tindakan ini harus dipanggil sebelum
panggilan sinkron dilakukan. Ini adalah panggilan non-pemblokiran sehingga koneksi mungkin tidak akan siap (atau mungkin tidak dapat dilakukan) pada saat Anda melakukan panggilan. Dalam hal ini, perilaku penanganan error yang biasa akan berlaku.
Jika Anda tidak memiliki izin lintas profil yang sesuai saat memanggil
ProfileConnector#addConnectionHolder(Object)
atau tidak ada profil yang tersedia untuk
terhubung, tidak akan ada error yang ditampilkan, tetapi callback yang terhubung tidak akan
dipanggil. Jika izin diberikan nanti atau profil lain menjadi
tersedia, koneksi akan dibuat dan callback akan dipanggil.
Atau, ProfileConnector#connect(Object)
adalah metode pemblokiran yang
akan menambahkan objek sebagai holder koneksi dan membuat koneksi atau
menampilkan UnavailableProfileException
. Metode ini tidak dapat dipanggil dari
UI Thread.
Panggilan ke ProfileConnector#connect(Object)
dan ProfileConnector#connect
serupa
akan menampilkan objek penutupan otomatis yang akan otomatis
menghapus holder koneksi setelah ditutup. Hal ini memungkinkan penggunaan seperti:
try (ProfileConnectionHolder p = connector.connect()) {
// Use the connection
}
Setelah selesai melakukan panggilan sinkron, Anda harus memanggil
ProfileConnector#removeConnectionHolder(Object)
. Setelah semua pemegang koneksi
dihapus, koneksi akan ditutup.
Konektivitas
Pemroses koneksi dapat digunakan untuk mendapatkan informasi saat status koneksi
berubah, dan connector.utils().isConnected
dapat digunakan untuk menentukan apakah
koneksi ada. Contoh:
// Only use this if using synchronous calls instead of Futures.
crossProfileConnector.connect(this);
crossProfileConnector.registerConnectionListener(() -> {
if (crossProfileConnector.utils().isConnected()) {
// Make cross-profile calls.
}
});
Panggilan Asinkron
Setiap metode yang ditampilkan di seluruh pemisah profil harus ditetapkan sebagai pemblokiran
(sinkron) atau non-pemblokiran (asinkron). Setiap metode yang menampilkan
jenis data asinkron (misalnya ListenableFuture
) atau menerima parameter
callback akan ditandai sebagai non-blocking. Semua metode lainnya ditandai sebagai pemblokiran.
Panggilan asinkron direkomendasikan. Jika Anda harus menggunakan panggilan sinkron, lihat Panggilan Sinkron.
Callback
Jenis panggilan non-pemblokiran yang paling dasar adalah metode void yang menerima sebagai salah satu
parameternya antarmuka yang berisi metode yang akan dipanggil dengan
hasilnya. Agar antarmuka ini berfungsi dengan SDK, antarmuka harus
dianotasi @CrossProfileCallback
. Contoh:
@CrossProfileCallback
public interface InstallationCompleteListener {
void installationComplete(int state);
}
Antarmuka ini kemudian dapat digunakan sebagai parameter dalam metode yang dianotasi
@CrossProfile
dan dipanggil seperti biasa. Contoh:
@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
});
Jika antarmuka ini berisi satu metode, yang menggunakan nol atau satu parameter, antarmuka ini juga dapat digunakan dalam panggilan ke beberapa profil sekaligus.
Jumlah nilai apa pun dapat diteruskan menggunakan callback, tetapi koneksi hanya akan terbuka untuk nilai pertama. Lihat Holder Koneksi untuk mengetahui informasi tentang menjaga koneksi tetap terbuka untuk menerima lebih banyak nilai.
Metode sinkron dengan callback
Salah satu fitur yang tidak biasa dari penggunaan callback dengan SDK adalah Anda secara teknis dapat menulis metode sinkron yang menggunakan callback:
public void install(InstallationCompleteListener callback) {
callback.installationComplete(1);
}
Dalam hal ini, metode sebenarnya sinkron, meskipun ada callback. Kode ini akan dieksekusi dengan benar:
System.out.println("This prints first");
installer.install(() -> {
System.out.println("This prints second");
});
System.out.println("This prints third");
Namun, saat dipanggil menggunakan SDK, perilakunya tidak akan sama. Tidak ada jaminan bahwa metode penginstalan akan dipanggil sebelum "Ini mencetak ketiga" dicetak. Setiap penggunaan metode yang ditandai sebagai asinkron oleh SDK tidak boleh membuat asumsi tentang kapan metode akan dipanggil.
Callback Sederhana
"Callback sederhana" adalah bentuk callback yang lebih ketat yang memungkinkan fitur tambahan saat melakukan panggilan lintas profil. Antarmuka sederhana harus berisi satu metode, yang dapat menggunakan nol atau satu parameter.
Anda dapat mewajibkan antarmuka callback untuk tetap ada dengan menentukan
simple=true
dalam anotasi @CrossProfileCallback
.
Callback sederhana dapat digunakan dengan berbagai metode seperti .both()
, .suppliers()
,
dan lainnya.
Holder Koneksi
Saat melakukan panggilan asinkron (menggunakan callback atau future), holder koneksi akan ditambahkan saat melakukan panggilan dan dihapus saat pengecualian atau nilai diteruskan.
Jika Anda mengharapkan lebih dari satu hasil diteruskan menggunakan callback, Anda harus menambahkan callback secara manual sebagai holder koneksi:
MyCallback b = //...
connector.addConnectionHolder(b);
profileMyClass.other().registerListener(b);
// Now the connection will be held open indefinitely, once finished:
connector.removeConnectionHolder(b);
Ini juga dapat digunakan dengan blok try-with-resources:
MyCallback b = //...
try (ProfileConnectionHolder p = connector.addConnectionHolder(b)) {
profileMyClass.other().registerListener(b);
// Other things running while we expect results
}
Jika kita melakukan panggilan dengan callback atau future, koneksi akan tetap terbuka hingga hasil diteruskan. Jika kami menentukan bahwa hasil tidak akan diteruskan, kita harus menghapus callback atau future sebagai holder koneksi:
connector.removeConnectionHolder(myCallback);
connector.removeConnectionHolder(future);
Untuk informasi selengkapnya, lihat Holder Koneksi.
Futures
Futures juga didukung secara native oleh SDK. Satu-satunya jenis Future yang didukung secara native adalah ListenableFuture
, meskipun jenis Future kustom dapat digunakan.
Untuk menggunakan future, Anda cukup mendeklarasikan jenis Future yang didukung sebagai jenis nilai yang ditampilkan
metode lintas profil, lalu menggunakannya seperti biasa.
Ini memiliki "fitur tidak biasa" yang sama dengan callback, dengan metode sinkron
yang menampilkan future (misalnya, menggunakan immediateFuture
) akan berperilaku berbeda
saat dijalankan di profil saat ini dibandingkan dijalankan di profil lain. Setiap penggunaan
metode yang ditandai sebagai asinkron oleh SDK tidak boleh membuat asumsi tentang kapan
metode akan dipanggil.
Thread
Jangan memblokir hasil callback atau masa depan lintas profil di thread utama. Jika Anda melakukannya, dalam beberapa situasi, kode Anda akan diblokir tanpa batas waktu. Hal ini karena koneksi ke profil lain juga dibuat di thread utama, yang tidak akan pernah terjadi jika diblokir menunggu hasil lintas profil.
Ketersediaan
Pemroses ketersediaan dapat digunakan untuk mendapatkan informasi saat status
ketersediaan berubah, dan connector.utils().isAvailable
dapat digunakan untuk menentukan apakah
profil lain tersedia untuk digunakan. Contoh:
crossProfileConnector.registerAvailabilityListener(() -> {
if (crossProfileConnector.utils().isAvailable()) {
// Show cross-profile content
} else {
// Hide cross-profile content
}
});
Holder Koneksi
Holder koneksi adalah objek arbitrer yang dicatat sebagai memiliki dan tertarik pada koneksi lintas profil yang dibuat dan dipertahankan.
Secara default, saat melakukan panggilan asinkron, holder koneksi akan ditambahkan saat panggilan dimulai, dan dihapus saat hasil atau error terjadi.
Holder Koneksi juga dapat ditambahkan dan dihapus secara manual untuk memberikan kontrol yang lebih besar
atas koneksi. Holder koneksi dapat ditambahkan menggunakan
connector.addConnectionHolder
, dan dihapus menggunakan
connector.removeConnectionHolder
.
Jika ada setidaknya satu holder koneksi yang ditambahkan, SDK akan mencoba mempertahankan koneksi. Jika tidak ada pemegang koneksi yang ditambahkan, koneksi dapat ditutup.
Anda harus mempertahankan referensi ke pemegang koneksi yang ditambahkan - dan menghapusnya jika tidak lagi relevan.
Panggilan tersinkron
Sebelum melakukan panggilan sinkron, holder koneksi harus ditambahkan. Hal ini dapat dilakukan menggunakan objek apa pun, meskipun Anda harus melacak objek tersebut agar dapat dihapus saat Anda tidak perlu lagi melakukan panggilan sinkron.
Panggilan asinkron
Saat melakukan panggilan asinkron, holder koneksi akan otomatis dikelola sehingga koneksi terbuka antara panggilan dan respons atau error pertama. Jika Anda memerlukan koneksi untuk tetap aktif setelah ini (misalnya, untuk menerima beberapa respons menggunakan satu callback), Anda harus menambahkan callback itu sendiri sebagai holder koneksi, dan menghapusnya setelah Anda tidak lagi perlu menerima data lebih lanjut.
Penanganan Error
Secara default, setiap panggilan yang dilakukan ke profil lain saat profil lain tidak
tersedia akan menyebabkan UnavailableProfileException
ditampilkan (atau
diteruskan ke Future, atau callback error untuk panggilan asinkron).
Untuk menghindari hal ini, developer dapat menggunakan #both()
atau #suppliers()
dan menulis
kode mereka untuk menangani jumlah entri dalam daftar yang dihasilkan (ini akan menjadi 1 jika
profil lainnya tidak tersedia, atau 2 jika tersedia).
Pengecualian
Setiap pengecualian yang tidak dicentang yang terjadi setelah panggilan ke profil saat ini akan
disebarkan seperti biasa. Hal ini berlaku terlepas dari metode yang digunakan untuk melakukan
panggilan (#current()
, #personal
, #both
, dll.).
Pengecualian yang tidak diperiksa yang terjadi setelah panggilan ke profil lain akan menyebabkan
ProfileRuntimeException
ditampilkan dengan pengecualian asli sebagai
penyebabnya. Hal ini berlaku terlepas dari metode yang digunakan untuk melakukan panggilan (#other()
,
#personal
, #both
, dll.).
ifAvailable
Sebagai alternatif untuk menangkap dan menangani instance UnavailableProfileException
, Anda dapat menggunakan metode .ifAvailable()
untuk memberikan nilai default yang akan ditampilkan, bukan menampilkan UnavailableProfileException
.
Contoh:
profileNotesDatabase.other().ifAvailable().getNumberOfNotes(/* defaultValue= */ 0);
Pengujian
Agar kode dapat diuji, Anda harus memasukkan instance konektor profil ke kode apa pun yang menggunakannya (untuk memeriksa ketersediaan profil, untuk terhubung secara manual, dll.). Anda juga harus memasukkan instance jenis yang mengetahui profil tempat jenis tersebut digunakan.
Kami menyediakan konektor dan jenis palsu yang dapat digunakan dalam pengujian.
Pertama, tambahkan dependensi pengujian:
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'
Kemudian, anotasikan class pengujian Anda dengan @CrossProfileTest
, yang mengidentifikasi
class beranotasi @CrossProfileConfiguration
yang akan diuji:
@CrossProfileTest(configuration = MyApplication.class)
@RunWith(RobolectricTestRunner.class)
public class NotesMediatorTest {
}
Tindakan ini akan menyebabkan pembuatan palsu untuk semua jenis dan konektor yang digunakan dalam konfigurasi.
Buat instance palsu tersebut dalam pengujian Anda:
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();
Siapkan status profil:
connector.setRunningOnProfile(PERSONAL);
connector.createWorkProfile();
connector.turnOffWorkProfile();
Teruskan konektor palsu dan class lintas profil ke kode yang sedang diuji, lalu lakukan panggilan.
Panggilan akan dirutekan ke target yang benar - dan pengecualian akan ditampilkan saat melakukan panggilan ke profil yang terputus atau tidak tersedia.
Jenis yang Didukung
Jenis berikut didukung tanpa upaya tambahan dari Anda. Ini dapat digunakan sebagai argumen atau jenis nilai yang ditampilkan untuk semua panggilan lintas profil.
- Primitif (
byte
,short
,int
,long
,float
,double
,char
,boolean
), - Primitif Berkotak (
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
,- Apa pun yang mengimplementasikan
android.os.Parcelable
, - Apa pun yang mengimplementasikan
java.io.Serializable
, - Array non-primitif satu dimensi,
java.util.Optional
,java.util.Collection
,java.util.List
,java.util.Map
,java.util.Set
,android.util.Pair
,com.google.common.collect.ImmutableMap
.
Setiap jenis generik yang didukung (misalnya java.util.Collection
) dapat memiliki
jenis yang didukung sebagai parameter jenisnya. Contoh:
java.util.Collection<java.util.Map<java.lang.String,MySerializableType[]>>
adalah
jenis yang valid.
Futures
Jenis berikut hanya didukung sebagai jenis nilai yang ditampilkan:
com.google.common.util.concurrent.ListenableFuture
Wrapper Parcelable Kustom
Jika jenis Anda tidak ada dalam daftar sebelumnya, pertimbangkan terlebih dahulu apakah jenis tersebut dapat dibuat untuk
mengimplementasikan android.os.Parcelable
atau java.io.Serializable
dengan benar. Jika
tidak dapat melihat wrapper parcelable untuk menambahkan dukungan untuk jenis Anda.
Wrapper Future Kustom
Jika Anda ingin menggunakan jenis mendatang yang tidak ada dalam daftar sebelumnya, lihat wrapper mendatang untuk menambahkan dukungan.
Wrapper Parcelable
Wrapper Parcelable adalah cara SDK menambahkan dukungan untuk jenis yang tidak dapat di-parcel dan tidak dapat diubah. SDK menyertakan wrapper untuk banyak jenis, tetapi jika jenis yang perlu Anda gunakan tidak disertakan, Anda harus menulis sendiri.
Wrapper Parcelable adalah class yang dirancang untuk menggabungkan class lain dan membuatnya dapat di-parcel. Class ini mengikuti kontrak statis yang ditentukan dan terdaftar dengan SDK sehingga dapat digunakan untuk mengonversi jenis tertentu menjadi jenis parcelable, dan juga mengekstrak jenis tersebut dari jenis parcelable.
Annotation
Class wrapper parcelable harus dianotasi @CustomParcelableWrapper
,
yang menentukan class yang digabungkan sebagai originalType
. Contoh:
@CustomParcelableWrapper(originalType=ImmutableList.class)
Format
Wrapper parcelable harus menerapkan Parcelable
dengan benar, dan harus memiliki
metode W of(Bundler, BundlerType, T)
statis yang menggabungkan jenis yang digabungkan dan
metode T get()
non-statis yang menampilkan jenis yang digabungkan.
SDK akan menggunakan metode ini untuk memberikan dukungan yang lancar untuk jenis tersebut.
Bundler
Untuk memungkinkan penggabungan jenis generik (seperti daftar dan peta), metode of
diteruskan Bundler
yang dapat membaca (menggunakan #readFromParcel
) dan
menulis (menggunakan #writeToParcel
) semua jenis yang didukung ke Parcel
, dan
BundlerType
yang mewakili jenis yang dideklarasikan untuk ditulis.
Instance Bundler
dan BundlerType
itu sendiri dapat di-parcel, dan harus
ditulis sebagai bagian dari pemisahan wrapper parcelable, sehingga dapat
digunakan saat merekonstruksi wrapper parcelable.
Jika BundlerType
mewakili jenis generik, variabel jenis dapat ditemukan
dengan memanggil .typeArguments()
. Setiap argumen jenis itu sendiri adalah BundlerType
.
Untuk contoh, lihat 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];
}
};
}
Mendaftar dengan SDK
Setelah dibuat, untuk menggunakan wrapper parcelable kustom, Anda harus mendaftarkannya dengan SDK.
Untuk melakukannya, tentukan parcelableWrappers={YourParcelableWrapper.class}
dalam
anotasi CustomProfileConnector
atau anotasi CrossProfile
pada class.
Wrapper Mendatang
Wrapper Masa Depan adalah cara SDK menambahkan dukungan untuk masa depan di seluruh profil. SDK
menyertakan dukungan untuk ListenableFuture
secara default, tetapi untuk jenis Future
lainnya, Anda dapat menambahkan dukungan sendiri.
Future Wrapper adalah class yang dirancang untuk menggabungkan jenis Future tertentu dan membuatnya tersedia untuk SDK. Ini mengikuti kontrak statis yang ditentukan dan harus didaftarkan dengan SDK.
Annotation
Class wrapper mendatang harus dianotasi @CustomFutureWrapper
, yang menentukan
class yang digabungkan sebagai originalType
. Contoh:
@CustomFutureWrapper(originalType=SettableFuture.class)
Format
Wrapper mendatang harus memperluas
com.google.android.enterprise.connectedapps.FutureWrapper
.
Wrapper mendatang harus memiliki metode W create(Bundler, BundlerType)
statis yang
membuat instance wrapper. Pada saat yang sama, tindakan ini akan membuat
instance jenis masa mendatang yang digabungkan. Ini harus ditampilkan oleh metode T
getFuture()
non-statis. Metode onResult(E)
dan onException(Throwable)
harus diimplementasikan untuk meneruskan hasil atau throwable ke future yang digabungkan.
Wrapper mendatang juga harus memiliki metode void writeFutureResult(Bundler,
BundlerType, T, FutureResultWriter<E>)
statis. Ini akan terdaftar dengan
yang diteruskan pada masa mendatang untuk hasil, dan saat hasil diberikan, panggil
resultWriter.onSuccess(value)
. Jika pengecualian diberikan,
resultWriter.onFailure(exception)
harus dipanggil.
Terakhir, wrapper mendatang juga harus memiliki metode T<Map<Profile, E>>
groupResults(Map<Profile, T<E>> results)
statis yang mengonversi peta dari
profil ke masa mendatang, menjadi masa mendatang peta dari profil ke hasil.
CrossProfileCallbackMultiMerger
dapat digunakan untuk mempermudah logika ini.
Contoh:
/** 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;
}
}
Mendaftar dengan SDK
Setelah dibuat, untuk menggunakan wrapper masa mendatang kustom, Anda harus mendaftarkannya dengan SDK.
Untuk melakukannya, tentukan futureWrappers={YourFutureWrapper.class}
dalam
anotasi CustomProfileConnector
atau anotasi CrossProfile
pada class.
Mode Direct Boot
Jika aplikasi Anda mendukung mode booting langsung, Anda mungkin perlu melakukan panggilan lintas profil sebelum profil dibuka kuncinya. Secara default, SDK hanya mengizinkan koneksi saat profil lain tidak terkunci.
Untuk mengubah perilaku ini, jika menggunakan konektor profil kustom, Anda
harus menentukan
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();
}
}
Jika Anda menggunakan CrossProfileConnector
, gunakan
.setAvailabilityRestrictions(AvailabilityRestrictions.DIRECT_BOOT
_AWARE
di
builder.
Dengan perubahan ini, Anda akan diberi tahu tentang ketersediaan, dan dapat melakukan panggilan lintas profil, saat profil lain tidak dibuka kuncinya. Anda bertanggung jawab untuk memastikan panggilan Anda hanya mengakses penyimpanan yang dienkripsi dengan perangkat.