Các phần này chỉ để tham khảo và bạn không bắt buộc phải đọc từ đầu đến cuối.
Yêu cầu sự đồng ý của người dùng
Sử dụng API khung:
CrossProfileApps.canInteractAcrossProfiles()
CrossProfileApps.canRequestInteractAcrossProfiles()
CrossProfileApps.createRequestInteractAcrossProfilesIntent()
CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED
Các API này sẽ được gói trong SDK để có giao diện API nhất quán hơn (ví dụ: tránh các đối tượng UserHandle), nhưng hiện tại, bạn có thể gọi trực tiếp các API này.
Cách triển khai rất đơn giản: nếu bạn có thể tương tác, hãy tiếp tục. Nếu không, nhưng bạn có thể yêu cầu, hãy hiển thị lời nhắc/biểu ngữ/chú giải công cụ/v.v. cho người dùng. Nếu người dùng đồng ý chuyển đến phần Cài đặt, hãy tạo ý định yêu cầu và sử dụng Context#startActivity
để chuyển người dùng đến đó. Bạn có thể sử dụng thông báo truyền tin để phát hiện thời điểm khả năng này thay đổi hoặc chỉ cần kiểm tra lại khi người dùng quay lại.
Để kiểm thử điều này, bạn cần mở TestDPC trong hồ sơ công việc, chuyển xuống cuối cùng rồi chọn thêm tên gói vào danh sách cho phép ứng dụng đã kết nối. Thao tác này mô phỏng việc quản trị viên "cho phép đưa vào danh sách" ứng dụng của bạn.
Bảng thuật ngữ
Phần này xác định các thuật ngữ chính liên quan đến việc phát triển trên nhiều hồ sơ.
Cấu hình trên nhiều hồ sơ
Cấu hình hồ sơ chéo nhóm các Lớp Nhà cung cấp hồ sơ chéo liên quan với nhau và cung cấp cấu hình chung cho các tính năng trên nhiều hồ sơ.
Thường thì mỗi cơ sở mã sẽ có một chú thích @CrossProfileConfiguration
, nhưng trong một số ứng dụng phức tạp, có thể có nhiều chú thích.
Trình kết nối hồ sơ
Trình kết nối quản lý các kết nối giữa các hồ sơ. Thông thường, mỗi loại hồ sơ chéo sẽ trỏ đến một Trình kết nối cụ thể. Mọi loại hồ sơ chéo trong một cấu hình phải sử dụng cùng một Trình kết nối.
Lớp nhà cung cấp trên nhiều hồ sơ
Lớp nhà cung cấp hồ sơ chéo nhóm các Loại hồ sơ chéo có liên quan với nhau.
Mediator
Trình dàn xếp nằm giữa mã cấp cao và cấp thấp, phân phối các lệnh gọi đến hồ sơ chính xác và hợp nhất kết quả. Đây là mã duy nhất cần nhận biết hồ sơ. Đây là một khái niệm về cấu trúc thay vì một nội dung được tích hợp vào SDK.
Loại hồ sơ chéo
Loại hồ sơ chéo là một lớp hoặc giao diện chứa các phương thức được chú thích bằng @CrossProfile
. Mã trong loại này không cần nhận biết hồ sơ và lý tưởng nhất là chỉ hoạt động trên dữ liệu cục bộ.
Loại hồ sơ
Loại hồ sơ | |
---|---|
Hiện tại | Hồ sơ đang hoạt động mà chúng ta đang thực thi. |
Khác | (nếu có) Hồ sơ mà chúng ta không thực thi. |
Cá nhân | Người dùng 0, hồ sơ trên thiết bị không thể tắt. |
Số nơi làm việc | Thông thường là người dùng 10 nhưng có thể cao hơn, có thể bật và tắt, dùng để chứa ứng dụng và dữ liệu công việc. |
Chính | Do ứng dụng xác định (không bắt buộc). Hồ sơ hiển thị chế độ xem hợp nhất của cả hai hồ sơ. |
Cấp hai | Nếu bạn xác định chính, thì phụ là hồ sơ không phải chính. |
Nhà cung cấp | Nhà cung cấp cho hồ sơ chính là cả hai hồ sơ, còn nhà cung cấp cho hồ sơ phụ chỉ là chính hồ sơ phụ. |
Mã nhận dạng hồ sơ
Một lớp đại diện cho một loại hồ sơ (cá nhân hoặc công việc). Các giá trị này sẽ được các phương thức chạy trên nhiều hồ sơ trả về và có thể được dùng để chạy thêm mã trên các hồ sơ đó. Bạn có thể chuyển đổi tuần tự các giá trị này thành int
để lưu trữ thuận tiện.
Giải pháp đề xuất về cấu trúc
Hướng dẫn này trình bày các cấu trúc được đề xuất để xây dựng các chức năng trên nhiều hồ sơ hiệu quả và dễ bảo trì trong ứng dụng Android.
Chuyển đổi CrossProfileConnector
thành singleton
Bạn chỉ nên sử dụng một thực thể trong suốt vòng đời của ứng dụng, nếu không bạn sẽ tạo các kết nối song song. Bạn có thể thực hiện việc này bằng cách sử dụng một khung chèn phần phụ thuộc như Dagger hoặc bằng cách sử dụng mẫu Singleton cổ điển, trong một lớp mới hoặc một lớp hiện có.
Chèn hoặc truyền thực thể Hồ sơ đã tạo vào lớp của bạn khi bạn thực hiện lệnh gọi, thay vì tạo thực thể đó trong phương thức
Điều này cho phép bạn truyền thực thể FakeProfile
được tạo tự động trong các bài kiểm thử đơn vị sau này.
Cân nhắc mẫu dàn xếp
Mẫu phổ biến này là để tạo một trong các API hiện có (ví dụ: getEvents()
) nhận biết hồ sơ cho tất cả phương thức gọi của API đó. Trong trường hợp này, API hiện có của bạn chỉ có thể trở thành một phương thức hoặc lớp "trung gian" chứa lệnh gọi mới để tạo mã trên nhiều hồ sơ.
Bằng cách này, bạn không buộc mọi phương thức gọi phải biết cách thực hiện lệnh gọi trên nhiều hồ sơ, mà chỉ cần trở thành một phần của API.
Cân nhắc việc chú thích phương thức giao diện là @CrossProfile
để tránh phải hiển thị các lớp triển khai trong trình cung cấp
Cách này hoạt động tốt với các khung chèn phần phụ thuộc.
Nếu bạn đang nhận được dữ liệu từ một lệnh gọi trên nhiều hồ sơ, hãy cân nhắc việc thêm một trường tham chiếu đến hồ sơ mà dữ liệu đó đến từ
Đây có thể là một phương pháp hay vì bạn có thể muốn biết điều này ở lớp giao diện người dùng (ví dụ: thêm biểu tượng huy hiệu vào công việc). Bạn cũng có thể cần mã nhận dạng này nếu không có mã nhận dạng này, bất kỳ giá trị nhận dạng dữ liệu nào cũng không còn duy nhất, chẳng hạn như tên gói.
Hồ sơ chéo
Phần này trình bày cách tạo các lượt tương tác trên nhiều hồ sơ của riêng bạn.
Hồ sơ chính
Hầu hết các lệnh gọi trong ví dụ trên tài liệu này đều chứa hướng dẫn rõ ràng về hồ sơ cần chạy, bao gồm cả hồ sơ công việc, hồ sơ cá nhân và cả hai.
Trong thực tế, đối với các ứng dụng có trải nghiệm hợp nhất trên chỉ một hồ sơ, bạn có thể muốn quyết định này phụ thuộc vào hồ sơ mà bạn đang chạy, vì vậy, có các phương thức thuận tiện tương tự cũng tính đến điều này, để tránh cơ sở mã của bạn bị lộn xộn với các điều kiện hồ sơ if-else.
Khi tạo thực thể trình kết nối, bạn có thể chỉ định loại hồ sơ nào là "chính" (ví dụ: "WORK"). Điều này cho phép các tuỳ chọn khác, chẳng hạn như:
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();
Các loại hồ sơ chéo
Các lớp và giao diện chứa một phương thức được chú thích @CrossProfile
được gọi là Loại hồ sơ chéo.
Việc triển khai các Loại hồ sơ chéo phải độc lập với hồ sơ, hồ sơ mà chúng đang chạy. Các phương thức này được phép gọi các phương thức khác và nói chung sẽ hoạt động như đang chạy trên một hồ sơ duy nhất. Họ sẽ chỉ có quyền truy cập vào trạng thái trong hồ sơ của riêng họ.
Ví dụ về Loại hồ sơ chéo:
public class Calculator {
@CrossProfile
public int add(int a, int b) {
return a + b;
}
}
Chú thích lớp
Để cung cấp API mạnh nhất, bạn nên chỉ định trình kết nối cho từng loại hồ sơ chéo, như sau:
@CrossProfile(connector=MyProfileConnector.class)
public class Calculator {
@CrossProfile
public int add(int a, int b) {
return a + b;
}
}
Đây là tuỳ chọn nhưng có nghĩa là API được tạo sẽ cụ thể hơn về các loại và nghiêm ngặt hơn về việc kiểm tra thời gian biên dịch.
Giao diện
Bằng cách chú thích các phương thức trên giao diện là @CrossProfile
, bạn đang tuyên bố rằng có thể có một số phương thức triển khai của phương thức này có thể truy cập được trên các hồ sơ.
Bạn có thể trả về bất kỳ phương thức triển khai nào của giao diện Hồ sơ chéo trong Nhà cung cấp hồ sơ chéo. Bằng cách đó, bạn đang cho biết rằng phương thức triển khai này phải có thể truy cập được trên nhiều hồ sơ. Bạn không cần chú thích các lớp triển khai.
Nhà cung cấp trên nhiều hồ sơ
Mỗi Loại hồ sơ chéo phải được cung cấp bằng một phương thức được chú thích @CrossProfileProvider
. Các phương thức này sẽ được gọi mỗi khi thực hiện lệnh gọi trên nhiều hồ sơ, vì vậy, bạn nên duy trì singleton cho từng loại.
Hàm dựng
Nhà cung cấp phải có một hàm khởi tạo công khai không nhận đối số hoặc chỉ nhận một đối số Context
.
Phương thức nhà cung cấp
Phương thức của trình cung cấp không được nhận đối số nào hoặc chỉ nhận một đối số Context
.
Chèn phần phụ thuộc
Nếu đang sử dụng một khung chèn phần phụ thuộc như Dagger để quản lý các phần phụ thuộc, bạn nên yêu cầu khung đó tạo các loại hồ sơ chéo như bình thường, sau đó chèn các loại đó vào lớp trình cung cấp. Sau đó, các phương thức @CrossProfileProvider
có thể trả về các thực thể được chèn đó.
Trình kết nối hồ sơ
Mỗi Cấu hình hồ sơ chéo phải có một Trình kết nối hồ sơ duy nhất, chịu trách nhiệm quản lý kết nối với hồ sơ khác.
Trình kết nối hồ sơ mặc định
Nếu chỉ có một Cấu hình hồ sơ chéo trong cơ sở mã, thì bạn có thể tránh tạo Trình kết nối hồ sơ của riêng mình và sử dụng com.google.android.enterprise.connectedapps.CrossProfileConnector
. Đây là giá trị mặc định được sử dụng nếu không có giá trị nào được chỉ định.
Khi tạo Trình kết nối hồ sơ chéo, bạn có thể chỉ định một số tuỳ chọn trên trình tạo:
Dịch vụ thực thi theo lịch
Nếu bạn muốn kiểm soát các luồng do SDK tạo, hãy sử dụng
#setScheduledExecutorService()
,Liên kết
Nếu bạn có nhu cầu cụ thể về việc liên kết hồ sơ, hãy sử dụng
#setBinder
. Trình kiểm soát chính sách thiết bị có thể chỉ sử dụng thuộc tính này.
Trình kết nối hồ sơ tuỳ chỉnh
Bạn sẽ cần một trình kết nối hồ sơ tuỳ chỉnh để có thể thiết lập một số cấu hình (sử dụng CustomProfileConnector
) và sẽ cần một trình kết nối nếu bạn cần nhiều trình kết nối trong một cơ sở mã (ví dụ: nếu bạn có nhiều quy trình, bạn nên sử dụng một trình kết nối cho mỗi quy trình).
Khi tạo ProfileConnector
, mã sẽ có dạng như sau:
@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
Để thay đổi tên của dịch vụ được tạo (nên được tham chiếu trong
AndroidManifest.xml
), hãy sử dụngserviceClassName=
.primaryProfile
Để chỉ định hồ sơ chính, hãy sử dụng
primaryProfile
.availabilityRestrictions
Để thay đổi các quy định hạn chế mà SDK đặt ra đối với kết nối và tình trạng có sẵn hồ sơ, hãy sử dụng
availabilityRestrictions
.
Trình kiểm soát chính sách thiết bị
Nếu ứng dụng của bạn là Trình điều khiển chính sách thiết bị, thì bạn phải chỉ định một thực thể của DpcProfileBinder
tham chiếu đến DeviceAdminReceiver
.
Nếu bạn đang triển khai trình kết nối trang doanh nghiệp của riêng mình:
@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();
}
}
hoặc sử dụng CrossProfileConnector
mặc định:
CrossProfileConnector connector =
CrossProfileConnector.builder(context).setBinder(new DpcProfileBinder(new
ComponentName("com.google.testdpc", "AdminReceiver"))).build();
Cấu hình trên nhiều hồ sơ
Chú giải @CrossProfileConfiguration
được dùng để liên kết tất cả các loại hồ sơ chéo với nhau bằng một trình kết nối để gửi các lệnh gọi phương thức một cách chính xác. Để thực hiện việc này, chúng ta chú thích một lớp bằng @CrossProfileConfiguration
trỏ đến mọi nhà cung cấp, như sau:
@CrossProfileConfiguration(providers = {TestProvider.class})
public abstract class TestApplication {
}
Thao tác này sẽ xác thực rằng đối với tất cả Các loại hồ sơ chéo, chúng có cùng một trình kết nối hồ sơ hoặc không có trình kết nối nào được chỉ định.
serviceSuperclass
Theo mặc định, dịch vụ được tạo sẽ sử dụng
android.app.Service
làm lớp cha. Nếu bạn cần một lớp khác (chính lớp này phải là lớp con củaandroid.app.Service
) làm lớp cấp cao, hãy chỉ địnhserviceSuperclass=
.serviceClass
Nếu bạn chỉ định, hệ thống sẽ không tạo dịch vụ nào. Giá trị này phải khớp với
serviceClassName
trong trình kết nối hồ sơ mà bạn đang sử dụng. Dịch vụ tuỳ chỉnh của bạn sẽ điều phối các lệnh gọi bằng cách sử dụng lớp_Dispatcher
đã tạo như sau:
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;
}
}
Bạn có thể sử dụng phương thức này nếu cần thực hiện thêm thao tác trước hoặc sau lệnh gọi trên nhiều hồ sơ.
Trình kết nối
Nếu đang sử dụng một trình kết nối khác với
CrossProfileConnector
mặc định, thì bạn phải chỉ định trình kết nối đó bằngconnector=
.
Chế độ hiển thị
Mọi phần của ứng dụng tương tác trên nhiều hồ sơ phải có thể xem Trình kết nối hồ sơ.
Lớp được chú thích @CrossProfileConfiguration
phải có thể xem mọi nhà cung cấp được sử dụng trong ứng dụng.
Lệnh gọi đồng bộ
SDK Ứng dụng được kết nối hỗ trợ các lệnh gọi đồng bộ (chặn) trong trường hợp không thể tránh khỏi. Tuy nhiên, việc sử dụng các lệnh gọi này có một số hạn chế (chẳng hạn như các lệnh gọi có thể bị chặn trong thời gian dài). Vì vậy, bạn nên tránh sử dụng lệnh gọi đồng bộ khi có thể. Để sử dụng lệnh gọi không đồng bộ, hãy xem phần Lệnh gọi không đồng bộ .
Phần tử giữ kết nối
Nếu đang sử dụng lệnh gọi đồng bộ, bạn phải đảm bảo rằng có một chủ sở hữu kết nối được đăng ký trước khi thực hiện lệnh gọi trên nhiều hồ sơ, nếu không, một ngoại lệ sẽ được gửi. Để biết thêm thông tin, hãy xem phần Chủ sở hữu kết nối.
Để thêm một chủ sở hữu kết nối, hãy gọi ProfileConnector#addConnectionHolder(Object)
với bất kỳ đối tượng nào (có thể là thực thể đối tượng đang thực hiện lệnh gọi trên nhiều hồ sơ). Thao tác này sẽ ghi lại rằng đối tượng này đang sử dụng kết nối và sẽ cố gắng kết nối. Bạn phải gọi phương thức này trước khi thực hiện bất kỳ lệnh gọi đồng bộ nào. Đây là lệnh gọi không chặn nên có thể kết nối sẽ không sẵn sàng (hoặc có thể không thể) vào thời điểm bạn thực hiện lệnh gọi. Trong trường hợp đó, hành vi xử lý lỗi thông thường sẽ được áp dụng.
Nếu bạn thiếu các quyền thích hợp trên nhiều hồ sơ khi gọi ProfileConnector#addConnectionHolder(Object)
hoặc không có hồ sơ nào để kết nối, thì sẽ không có lỗi nào xảy ra nhưng lệnh gọi lại đã kết nối sẽ không bao giờ được gọi. Nếu quyền này được cấp sau đó hoặc hồ sơ khác có sẵn, thì kết nối sẽ được thực hiện và lệnh gọi lại sẽ được gọi.
Ngoài ra, ProfileConnector#connect(Object)
là một phương thức chặn sẽ thêm đối tượng làm chủ sở hữu kết nối và thiết lập kết nối hoặc gửi UnavailableProfileException
. Không thể gọi phương thức này từ
Luồng giao diện người dùng.
Các lệnh gọi đến ProfileConnector#connect(Object)
và ProfileConnector#connect
tương tự sẽ trả về các đối tượng tự động đóng, các đối tượng này sẽ tự động xoá trình giữ kết nối sau khi đóng. Điều này cho phép sử dụng như:
try (ProfileConnectionHolder p = connector.connect()) {
// Use the connection
}
Sau khi thực hiện xong các lệnh gọi đồng bộ, bạn nên gọi ProfileConnector#removeConnectionHolder(Object)
. Sau khi tất cả chủ sở hữu kết nối bị xoá, kết nối sẽ bị đóng.
Kết nối
Bạn có thể sử dụng trình nghe kết nối để được thông báo khi trạng thái kết nối thay đổi và có thể sử dụng connector.utils().isConnected
để xác định xem có kết nối hay không. Ví dụ:
// Only use this if using synchronous calls instead of Futures.
crossProfileConnector.connect(this);
crossProfileConnector.registerConnectionListener(() -> {
if (crossProfileConnector.utils().isConnected()) {
// Make cross-profile calls.
}
});
Lệnh gọi không đồng bộ
Mọi phương thức hiển thị trên đường phân chia hồ sơ phải được chỉ định là chặn (đồng bộ) hoặc không chặn (không đồng bộ). Mọi phương thức trả về một loại dữ liệu không đồng bộ (ví dụ: ListenableFuture
) hoặc chấp nhận tham số gọi lại đều được đánh dấu là không chặn. Tất cả các phương thức khác đều được đánh dấu là chặn.
Bạn nên sử dụng lệnh gọi không đồng bộ. Nếu bạn phải sử dụng lệnh gọi đồng bộ, hãy xem phần Lệnh gọi đồng bộ.
Lệnh gọi lại
Loại lệnh gọi không chặn cơ bản nhất là phương thức rỗng, chấp nhận một giao diện chứa phương thức được gọi cùng với kết quả làm một trong các tham số. Để các giao diện này hoạt động với SDK, giao diện phải được chú thích @CrossProfileCallback
. Ví dụ:
@CrossProfileCallback
public interface InstallationCompleteListener {
void installationComplete(int state);
}
Sau đó, bạn có thể sử dụng giao diện này làm tham số trong phương thức chú thích @CrossProfile
và gọi như bình thường. Ví dụ:
@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
});
Nếu giao diện này chứa một phương thức duy nhất, phương thức này sẽ nhận 0 hoặc 1 tham số, thì phương thức này cũng có thể được dùng trong các lệnh gọi đến nhiều hồ sơ cùng một lúc.
Bạn có thể truyền số lượng giá trị bất kỳ bằng lệnh gọi lại, nhưng kết nối sẽ chỉ được giữ mở cho giá trị đầu tiên. Hãy xem phần Chủ thể kết nối để biết thông tin về cách giữ kết nối mở để nhận thêm giá trị.
Phương thức đồng bộ có lệnh gọi lại
Một tính năng bất thường khi sử dụng lệnh gọi lại với SDK là về mặt kỹ thuật, bạn có thể viết một phương thức đồng bộ sử dụng lệnh gọi lại:
public void install(InstallationCompleteListener callback) {
callback.installationComplete(1);
}
Trong trường hợp này, phương thức này thực sự là đồng bộ, mặc dù có lệnh gọi lại. Mã này sẽ thực thi chính xác:
System.out.println("This prints first");
installer.install(() -> {
System.out.println("This prints second");
});
System.out.println("This prints third");
Tuy nhiên, khi được gọi bằng SDK, phương thức này sẽ không hoạt động theo cách tương tự. Không có gì đảm bảo rằng phương thức cài đặt sẽ được gọi trước khi in "This prints third" (Đây là lần in thứ ba). Mọi trường hợp sử dụng một phương thức được SDK đánh dấu là không đồng bộ đều không được giả định về thời điểm phương thức đó được gọi.
Lệnh gọi lại đơn giản
"Lệnh gọi lại đơn giản" là một dạng lệnh gọi lại hạn chế hơn, cho phép thêm các tính năng khi thực hiện lệnh gọi trên nhiều hồ sơ. Giao diện đơn giản phải chứa một phương thức duy nhất, có thể nhận 0 hoặc 1 tham số.
Bạn có thể thực thi việc giao diện gọi lại phải tồn tại bằng cách chỉ định simple=true
trong chú giải @CrossProfileCallback
.
Bạn có thể sử dụng lệnh gọi lại đơn giản với nhiều phương thức như .both()
, .suppliers()
và các phương thức khác.
Phần tử giữ kết nối
Khi thực hiện lệnh gọi không đồng bộ (sử dụng lệnh gọi lại hoặc tương lai), một trình giữ kết nối sẽ được thêm khi thực hiện lệnh gọi và bị xoá khi một ngoại lệ hoặc giá trị được truyền.
Nếu muốn truyền nhiều kết quả bằng lệnh gọi lại, bạn nên thêm lệnh gọi lại theo cách thủ công làm trình giữ kết nối:
MyCallback b = //...
connector.addConnectionHolder(b);
profileMyClass.other().registerListener(b);
// Now the connection will be held open indefinitely, once finished:
connector.removeConnectionHolder(b);
Bạn cũng có thể sử dụng phương thức này với khối try-with-resources:
MyCallback b = //...
try (ProfileConnectionHolder p = connector.addConnectionHolder(b)) {
profileMyClass.other().registerListener(b);
// Other things running while we expect results
}
Nếu chúng ta thực hiện lệnh gọi bằng lệnh gọi lại hoặc trong tương lai, thì kết nối sẽ được giữ mở cho đến khi kết quả được truyền. Nếu xác định rằng kết quả sẽ không được truyền, thì chúng ta nên xoá lệnh gọi lại hoặc tương lai làm chủ sở hữu kết nối:
connector.removeConnectionHolder(myCallback);
connector.removeConnectionHolder(future);
Để biết thêm thông tin, hãy xem phần Chủ sở hữu kết nối.
Futures
SDK cũng hỗ trợ các hợp đồng tương lai gốc. Loại Future (Tương lai) duy nhất được hỗ trợ gốc là ListenableFuture
, mặc dù bạn có thể sử dụng các loại Future tuỳ chỉnh.
Để sử dụng futures, bạn chỉ cần khai báo một loại Future được hỗ trợ làm loại dữ liệu trả về của một phương thức hồ sơ chéo, sau đó sử dụng như bình thường.
Phương thức này có "tính năng bất thường" giống như lệnh gọi lại, trong đó một phương thức đồng bộ trả về một kết quả trong tương lai (ví dụ: sử dụng immediateFuture
) sẽ hoạt động khác nhau khi chạy trên hồ sơ hiện tại so với khi chạy trên một hồ sơ khác. Mọi hoạt động sử dụng một phương thức được SDK đánh dấu là không đồng bộ đều không được giả định về thời điểm phương thức đó sẽ được gọi.
Luồng
Đừng chặn kết quả của một lệnh gọi lại hoặc tương lai trên nhiều hồ sơ trên luồng chính. Nếu bạn làm như vậy, thì trong một số trường hợp, mã của bạn sẽ chặn vô thời hạn. Điều này là do kết nối với hồ sơ khác cũng được thiết lập trên luồng chính. Điều này sẽ không bao giờ xảy ra nếu luồng chính bị chặn trong khi chờ kết quả trên nhiều hồ sơ.
Phạm vi cung cấp
Bạn có thể dùng trình nghe tình trạng có sẵn để được thông báo khi trạng thái có sẵn thay đổi và có thể dùng connector.utils().isAvailable
để xác định xem có thể sử dụng hồ sơ khác hay không. Ví dụ:
crossProfileConnector.registerAvailabilityListener(() -> {
if (crossProfileConnector.utils().isAvailable()) {
// Show cross-profile content
} else {
// Hide cross-profile content
}
});
Phần tử giữ kết nối
Chủ sở hữu kết nối là các đối tượng tuỳ ý được ghi nhận là có và quan tâm đến kết nối xuyên hồ sơ đang được thiết lập và duy trì.
Theo mặc định, khi thực hiện lệnh gọi không đồng bộ, một trình giữ kết nối sẽ được thêm vào khi lệnh gọi bắt đầu và bị xoá khi có bất kỳ kết quả hoặc lỗi nào xảy ra.
Bạn cũng có thể thêm và xoá Chủ sở hữu kết nối theo cách thủ công để kiểm soát tốt hơn kết nối. Bạn có thể thêm chủ sở hữu kết nối bằng cách sử dụng connector.addConnectionHolder
và xoá bằng connector.removeConnectionHolder
.
Khi có ít nhất một chủ sở hữu kết nối được thêm, SDK sẽ cố gắng duy trì kết nối. Khi không có chủ sở hữu kết nối nào được thêm, bạn có thể đóng kết nối.
Bạn phải duy trì tham chiếu đến mọi chủ sở hữu kết nối mà bạn thêm và xoá tham chiếu đó khi không còn liên quan nữa.
Lệnh gọi đồng bộ
Trước khi thực hiện lệnh gọi đồng bộ, bạn phải thêm một trình giữ kết nối. Bạn có thể thực hiện việc này bằng bất kỳ đối tượng nào, mặc dù bạn phải theo dõi đối tượng đó để có thể xoá đối tượng đó khi không cần thực hiện lệnh gọi đồng bộ nữa.
Lệnh gọi không đồng bộ
Khi thực hiện lệnh gọi không đồng bộ, trình giữ kết nối sẽ được tự động quản lý để kết nối được mở giữa lệnh gọi và phản hồi hoặc lỗi đầu tiên. Nếu cần kết nối tồn tại sau thời điểm này (ví dụ: để nhận nhiều phản hồi bằng một lệnh gọi lại), bạn nên thêm chính lệnh gọi lại đó làm chủ sở hữu kết nối và xoá lệnh gọi lại đó khi không cần nhận thêm dữ liệu nữa.
Xử lý lỗi
Theo mặc định, mọi lệnh gọi được thực hiện đến hồ sơ khác khi hồ sơ khác không có sẵn sẽ dẫn đến việc UnavailableProfileException
bị gửi (hoặc được truyền vào Future hoặc lệnh gọi lại lỗi cho lệnh gọi không đồng bộ).
Để tránh điều này, nhà phát triển có thể sử dụng #both()
hoặc #suppliers()
và viết mã để xử lý số lượng mục bất kỳ trong danh sách kết quả (số này sẽ là 1 nếu hồ sơ khác không có sẵn hoặc 2 nếu hồ sơ đó có sẵn).
Ngoại lệ
Mọi ngoại lệ chưa đánh dấu xảy ra sau lệnh gọi đến hồ sơ hiện tại sẽ được truyền như bình thường. Điều này áp dụng bất kể phương thức dùng để thực hiện lệnh gọi (#current()
, #personal
, #both
, v.v.).
Các ngoại lệ chưa kiểm tra xảy ra sau lệnh gọi đến hồ sơ khác sẽ dẫn đến việc ProfileRuntimeException
được gửi với ngoại lệ ban đầu là nguyên nhân. Điều này áp dụng bất kể phương thức dùng để thực hiện lệnh gọi (#other()
, #personal
, #both
, v.v.).
ifAvailable
Thay vì phát hiện và xử lý các thực thể UnavailableProfileException
, bạn có thể sử dụng phương thức .ifAvailable()
để cung cấp một giá trị mặc định sẽ được trả về thay vì gửi một UnavailableProfileException
.
Ví dụ:
profileNotesDatabase.other().ifAvailable().getNumberOfNotes(/* defaultValue= */ 0);
Thử nghiệm
Để có thể kiểm thử mã, bạn nên chèn các thực thể của trình kết nối hồ sơ vào bất kỳ mã nào sử dụng trình kết nối đó (để kiểm tra xem hồ sơ có sẵn hay không, để kết nối theo cách thủ công, v.v.). Bạn cũng nên chèn các thực thể của các loại nhận biết hồ sơ tại nơi chúng được sử dụng.
Chúng tôi cung cấp các loại và trình kết nối giả có thể dùng trong kiểm thử.
Trước tiên, hãy thêm các phần phụ thuộc kiểm thử:
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'
Sau đó, chú giải lớp kiểm thử bằng @CrossProfileTest
, xác định lớp được chú giải @CrossProfileConfiguration
cần kiểm thử:
@CrossProfileTest(configuration = MyApplication.class)
@RunWith(RobolectricTestRunner.class)
public class NotesMediatorTest {
}
Điều này sẽ dẫn đến việc tạo các tệp giả mạo cho tất cả các loại và trình kết nối được sử dụng trong cấu hình.
Tạo các thực thể của những giá trị giả mạo đó trong kiểm thử:
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();
Thiết lập trạng thái hồ sơ:
connector.setRunningOnProfile(PERSONAL);
connector.createWorkProfile();
connector.turnOffWorkProfile();
Truyền trình kết nối giả và lớp hồ sơ chéo vào mã đang kiểm thử, sau đó thực hiện lệnh gọi.
Các lệnh gọi sẽ được định tuyến đến đúng mục tiêu và các trường hợp ngoại lệ sẽ được gửi khi thực hiện lệnh gọi đến các hồ sơ bị ngắt kết nối hoặc không có sẵn.
Loài thuộc phạm vi
Bạn không cần phải làm gì thêm mà các loại sau đây vẫn được hỗ trợ. Bạn có thể sử dụng các loại này làm đối số hoặc loại dữ liệu trả về cho tất cả các lệnh gọi trên nhiều hồ sơ.
- Nguyên hàm (
byte
,short
,int
,long
,float
,double
,char
,boolean
), - Các đối tượng nguyên gốc dạng hộp (
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
,- Bất cứ nội dung nào triển khai
android.os.Parcelable
, - Bất cứ nội dung nào triển khai
java.io.Serializable
, - Mảng không nguyên gốc một chiều,
java.util.Optional
,java.util.Collection
,java.util.List
,java.util.Map
,java.util.Set
,android.util.Pair
,com.google.common.collect.ImmutableMap
.
Mọi loại chung được hỗ trợ (ví dụ: java.util.Collection
) đều có thể có bất kỳ loại nào được hỗ trợ làm tham số loại. Ví dụ:
java.util.Collection<java.util.Map<java.lang.String,MySerializableType[]>>
là một loại hợp lệ.
Futures
Các loại sau đây chỉ được hỗ trợ dưới dạng loại dữ liệu trả về:
com.google.common.util.concurrent.ListenableFuture
Trình bao bọc tuỳ chỉnh có thể phân phối
Nếu loại của bạn không có trong danh sách trước đó, trước tiên, hãy cân nhắc xem loại đó có thể được tạo để triển khai chính xác android.os.Parcelable
hoặc java.io.Serializable
hay không. Nếu không thể thấy các trình bao bọc có thể phân phối để thêm tính năng hỗ trợ cho loại của bạn.
Trình bao bọc tuỳ chỉnh trong tương lai
Nếu bạn muốn sử dụng một loại trong tương lai không có trong danh sách trước đó, hãy xem trình bao bọc trong tương lai để thêm tính năng hỗ trợ.
Trình bao bọc có thể phân phối
Trình bao bọc có thể phân phối là cách SDK thêm tính năng hỗ trợ cho các loại không phân phối được và không thể sửa đổi. SDK bao gồm các trình bao bọc cho nhiều loại, nhưng nếu loại bạn cần sử dụng không có trong SDK, bạn phải tự viết.
Trình bao bọc có thể phân phối là một lớp được thiết kế để bao bọc một lớp khác và giúp lớp đó có thể phân phối. Lớp này tuân theo một hợp đồng tĩnh đã xác định và được đăng ký với SDK để có thể dùng để chuyển đổi một loại nhất định thành loại có thể phân phối, đồng thời trích xuất loại đó từ loại có thể phân phối.
Annotation
Lớp trình bao bọc có thể phân phối phải được chú thích @CustomParcelableWrapper
, chỉ định lớp được bao bọc là originalType
. Ví dụ:
@CustomParcelableWrapper(originalType=ImmutableList.class)
Định dạng
Trình bao bọc có thể phân phối phải triển khai Parcelable
một cách chính xác và phải có phương thức W of(Bundler, BundlerType, T)
tĩnh để bao bọc loại được bao bọc và phương thức T get()
không tĩnh để trả về loại được bao bọc.
SDK sẽ sử dụng các phương thức này để hỗ trợ liền mạch cho loại này.
Trình tạo gói
Để cho phép gói các loại chung (chẳng hạn như danh sách và bản đồ), phương thức of
được truyền một Bundler
có thể đọc (sử dụng #readFromParcel
) và ghi (sử dụng #writeToParcel
) tất cả các loại được hỗ trợ vào Parcel
và một BundlerType
đại diện cho loại đã khai báo cần ghi.
Các thực thể Bundler
và BundlerType
tự nó có thể phân phối và phải được viết trong quá trình phân phối trình bao bọc có thể phân phối để có thể sử dụng khi tạo lại trình bao bọc có thể phân phối.
Nếu BundlerType
biểu thị một loại chung, bạn có thể tìm thấy các biến loại bằng cách gọi .typeArguments()
. Mỗi đối số loại đều là một BundlerType
.
Để biết ví dụ, hãy xem 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];
}
};
}
Đăng ký bằng SDK
Sau khi tạo, để sử dụng trình bao bọc có thể phân phối tuỳ chỉnh, bạn cần đăng ký trình bao bọc đó với SDK.
Để thực hiện việc này, hãy chỉ định parcelableWrappers={YourParcelableWrapper.class}
trong chú thích CustomProfileConnector
hoặc chú thích CrossProfile
trên một lớp.
Trình bao bọc trong tương lai
Trình bao bọc tương lai là cách SDK thêm tính năng hỗ trợ cho các tương lai trên các hồ sơ. Theo mặc định, SDK hỗ trợ ListenableFuture
, nhưng đối với các loại Future khác, bạn có thể tự thêm tính năng hỗ trợ.
Future Wrapper là một lớp được thiết kế để gói một loại Future cụ thể và cung cấp cho SDK. Lớp này tuân theo một hợp đồng tĩnh đã xác định và phải được đăng ký với SDK.
Annotation
Lớp trình bao bọc trong tương lai phải được chú thích @CustomFutureWrapper
, chỉ định lớp được bao bọc là originalType
. Ví dụ:
@CustomFutureWrapper(originalType=SettableFuture.class)
Định dạng
Các trình bao bọc trong tương lai phải mở rộng com.google.android.enterprise.connectedapps.FutureWrapper
.
Các trình bao bọc trong tương lai phải có một phương thức W create(Bundler, BundlerType)
tĩnh tạo một thực thể của trình bao bọc. Đồng thời, thao tác này sẽ tạo một thực thể của loại tương lai được gói. Giá trị này sẽ được trả về bởi phương thức T
getFuture()
không tĩnh. Bạn phải triển khai các phương thức onResult(E)
và onException(Throwable)
để truyền kết quả hoặc đối tượng có thể gửi đến tương lai được gói.
Các trình bao bọc trong tương lai cũng phải có phương thức void writeFutureResult(Bundler,
BundlerType, T, FutureResultWriter<E>)
tĩnh. Thao tác này sẽ đăng ký với giá trị được truyền trong tương lai cho kết quả và khi có kết quả, hãy gọi resultWriter.onSuccess(value)
. Nếu có một ngoại lệ, bạn nên gọi resultWriter.onFailure(exception)
.
Cuối cùng, trình bao bọc trong tương lai cũng phải có phương thức T<Map<Profile, E>>
groupResults(Map<Profile, T<E>> results)
tĩnh để chuyển đổi một bản đồ từ hồ sơ sang tương lai, thành một bản đồ tương lai từ hồ sơ sang kết quả.
Bạn có thể sử dụng CrossProfileCallbackMultiMerger
để giúp logic này dễ dàng hơn.
Ví dụ:
/** 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;
}
}
Đăng ký bằng SDK
Sau khi tạo, để sử dụng trình bao bọc tuỳ chỉnh trong tương lai, bạn cần đăng ký trình bao bọc đó với SDK.
Để thực hiện việc này, hãy chỉ định futureWrappers={YourFutureWrapper.class}
trong chú thích CustomProfileConnector
hoặc chú thích CrossProfile
trên một lớp.
Chế độ Khởi động trực tiếp
Nếu ứng dụng của bạn hỗ trợ chế độ khởi động trực tiếp, thì bạn có thể cần thực hiện các lệnh gọi trên nhiều hồ sơ trước khi hồ sơ được mở khoá. Theo mặc định, SDK chỉ cho phép kết nối khi hồ sơ khác được mở khoá.
Để thay đổi hành vi này, nếu đang sử dụng trình kết nối hồ sơ tuỳ chỉnh, bạn nên chỉ định 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();
}
}
Nếu bạn đang sử dụng CrossProfileConnector
, hãy sử dụng .setAvailabilityRestrictions(AvailabilityRestrictions.DIRECT_BOOT
_AWARE
trên trình tạo.
Với thay đổi này, bạn sẽ được thông báo về tình trạng có sẵn và có thể thực hiện các lệnh gọi trên nhiều hồ sơ khi hồ sơ kia chưa được mở khoá. Bạn có trách nhiệm đảm bảo rằng các lệnh gọi của bạn chỉ truy cập vào bộ nhớ được mã hoá của thiết bị.