הקטעים האלה מיועדים לעיון ולא צריך לקרוא אותם מלמעלה למטה.
בקשה להסכמת המשתמשים
שימוש בממשקי API של מסגרות:
CrossProfileApps.canInteractAcrossProfiles()
CrossProfileApps.canRequestInteractAcrossProfiles()
CrossProfileApps.createRequestInteractAcrossProfilesIntent()
CrossProfileApps.ACTION_CAN_INTERACT_ACROSS_PROFILES_CHANGED
ממשקי ה-API האלה ייכללו ב-SDK כדי ליצור פלטפורמת API עקבית יותר (למשל, כדי להימנע מאובייקטים של UserHandle, אבל בינתיים אפשר להתקשר אליהם ישירות.
ההטמעה פשוטה. אם יש לכם אפשרות ליצור אינטראקציה, אתם יכולים להמשיך בתהליך. אם לא, אבל אתם יכולים לבקש, תוכלו להציג למשתמש את ההנחיה/הבאנר/הטיפ וכו'. אם המשתמש מסכים לעבור להגדרות, תוכלו ליצור את כוונה הבקשה ולהשתמש ב-Context#startActivity
כדי לשלוח את המשתמש לשם. אתם יכולים להשתמש בשידור כדי לזהות מתי היכולת הזו משתנה, או פשוט לבדוק שוב כשהמשתמש יחזור.
כדי לבדוק את זה, צריך לפתוח את TestDPC בפרופיל העבודה, לעבור לתחתית המסך ולבחור להוסיף את שם החבילה לרשימת ההיתרים של האפליקציות המקושרות. הזה מחקה את רישום האדמין 'allow-listing' באפליקציה שלך.
מילון מונחים
הקטע הזה מגדיר מונחי מפתח הקשורים לפיתוח חוצה-פרופילים.
הגדרה בין פרופילים
הגדרה של הצגת היומן האישי בפרופיל העבודה מקבצת יחד כיתות קשורות של ספקי הצגת היומן האישי בפרופיל העבודה, ומספקת הגדרה כללית של התכונות האלה.
בדרך כלל תהיה הערה אחת מסוג @CrossProfileConfiguration
לכל קוד בסיס, אבל באפליקציות מורכבות יכולות להיות כמה הערות כאלה.
מחבר הפרופיל
המחבר מנהל את החיבורים בין הפרופילים. בדרך כלל כל פרופיל אחר יצביעו על מחבר ספציפי. כל סוג של פרופיל עסק אחד חייבים להשתמש באותו מחבר.
סוג ספק 'פרופיל משותף'
סוג הספק של הצגת היומן האישי בפרופיל העבודה מקבצת יחד סוגים קשורים של הצגת היומן האישי בפרופיל העבודה.
Mediator
מתווך נמצא בין קוד ברמה גבוהה לקוד ברמה נמוכה, ומחלק את הקריאות לפרופילים הנכונים וממזג את התוצאות. זה הקוד היחיד שצריך תוך התייחסות לפרופילים. זהו רעיון ארכיטקטוני ולא משהו שמובנה ב-SDK.
סוג הפרופיל המשולב
סוג של פרופילים שונים הוא מחלקה או ממשק שמכילים שיטות עם הערות
@CrossProfile
הקוד מסוג זה לא צריך להיות מודע לפרופיל, ובאופן אידיאלי הוא צריך לפעול רק על הנתונים המקומיים שלו.
סוגי פרופילים
סוג הפרופיל | |
---|---|
היחס הנוכחי | הפרופיל הפעיל שבו אנחנו מבצעים את הפעולה. |
אחר | (אם הוא קיים) הפרופיל שבו לא מבצעים את הפקודה. |
Personal | משתמש 0, הפרופיל במכשיר שלא ניתן להשבית. |
עבודה | בדרך כלל משתמש 10, אבל יכול להיות גם מספר גבוה יותר. אפשר להפעיל או להשבית אותו, והוא משמש לאפליקציות ולנתונים של העבודה. |
ראשי | אופציונלי: מוגדר על ידי האפליקציה. הפרופיל שבו מוצגת תצוגה משולבת של שני הפרופילים. |
Secondary | אם מוגדרת 'ראשי', 'משני' הוא הפרופיל שאינו ראשי. |
ספק | הספקים של הפרופיל הראשי הם שני הפרופילים, הספקים של הפרופיל המשני הוא רק הפרופיל המשני עצמו. |
מזהה הפרופיל
כיתה שמייצגת סוג של פרופיל (אישי או עבודה). הם יחזרו על ידי שיטות שפועלות בכמה פרופילים, וניתן להשתמש בהם כדי להריץ עוד קוד בפרופילים האלה. אפשר לסדר אותם בקובץ int
כדי שיהיה נוח
אחסון.
פתרונות מומלצים לארכיטקטורה
במדריך הזה מפורטים מבנים מומלצים ליצירת פונקציונליות חוצת-פרופילים באפליקציה ל-Android.
המרת CrossProfileConnector
לסינגלטון
מומלץ להשתמש רק במופע אחד במהלך מחזור החיים של אחרת, ייווצרו חיבורים מקבילים. אפשר לעשות את זה באמצעות מסגרת החדרת תלות כמו Dagger, או על ידי שימוש Singleton קלאסי דוגמת עיצוב, בכיתה חדשה או בכיתה קיימת.
להחדיר או להעביר את מופע הפרופיל שנוצר לכיתה שלכם כשאתם מבצעים את הקריאה, במקום ליצור אותו בשיטה
כך תוכלו להעביר את המופע של FakeProfile
שנוצר באופן אוטומטי מאוחר יותר בבדיקות היחידה.
שימוש בתבנית המגשר
התבנית הנפוצה הזו היא להפוך אחד מממשקי ה-API הקיימים (למשל getEvents()
) למודע לפרופיל לכל מבצעי הקריאה שלו. במקרה הזה, ה-API הקיים יכול
להפוך ל'מתווך' ה-method או המחלקה שמכילים את הקריאה החדשה שנוצרה
שמקושר לפרופילים שונים.
כך לא תצטרכו לאלץ כל מבצע קריאה לדעת איך לבצע קריאה בכמה פרופילים – היא פשוט תהפוך לחלק מ-API שלכם.
כדאי לשקול להוסיף הערה לשיטת ממשק כ-@CrossProfile
במקום זאת, כדי להימנע מהצורך לחשוף את מחלקות ההטמעה שלכם בספק
זה עובד טוב עם frameworks של החדרת תלות.
אם אתם מקבלים נתונים מבקשות מפרופילים שונים, כדאי לשקול להוסיף שדה שמציין את הפרופיל שממנו הם הגיעו
זוהי שיטה מומלצת, כי יכול להיות שתרצו לדעת את זה בשכבת ממשק המשתמש (למשל, הוספת סמל תג לדברים שקשורים לעבודה). יכול להיות שתצטרכו להשתמש בו גם אם מזהים של נתונים מסוימים כבר לא ייחודיים בלעדיו, כמו שמות חבילות.
חוצה פרופילים
בקטע הזה מוסבר איך ליצור אינטראקציות משלכם בין פרופילים.
פרופילים ראשיים
רוב הקריאות בדוגמאות במסמך הזה מכילות הוראות מפורשות לגבי הפרופיל שבו צריך להריץ את הקריאה, כולל פרופיל עבודה, פרופיל אישי או שניהם.
בפועל, באפליקציות שבהן חוויית המשתמש ממוזגת בפרופיל אחד בלבד, שההחלטה הזאת תהיה תלויה בפרופיל שבו אתם משתמשים, שיטות נוחות דומות שמביאות זאת בחשבון, כדי להימנע ש-codebase כבר עמוס בתנאים של פרופיל 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 בקישור ספק הפרופיל ובעצם זה נאמר היישום הזה צריך להיות נגיש בכל הפרופילים. לא צריך מוסיפים הערות למחלקות ההטמעה.
ספקים במספר פרופילים
כל סוג פרופיל חוצה חייב להיות מסופק על ידי שיטה עם הערה @CrossProfileProvider
. המערכת תקרא ל-methods האלה בכל פעם
מתבצעת קריאה חוצת-פרופילים, ולכן מומלץ לשמור על
לכל סוג.
יצרן
לספק חייב להיות קונסטרוקטור ציבורי שלא מקבל אף ארגומנט או ארגומנט Context
יחיד.
שיטות הספק
שיטות של ספקים לא יכולות לקבל אף ארגומנט או ארגומנט Context
יחיד.
הזרקת תלות
אם אתם משתמשים במסגרת להזרקת יחסי תלות כמו Dagger כדי לנהל את יחסי התלות, מומלץ להשתמש במסגרת הזו כדי ליצור את הסוגים שלכם בפרופיל, כפי שאתם עושים בדרך כלל, ואז להזריק את הסוגים האלה לכיתה של הספק. לאחר מכן, השיטות @CrossProfileProvider
יכולות להחזיר אותן
והחדרת מכונות.
מחבר הפרופיל
לכל הגדרה אישית של מספר פרופילים חייב להיות מחבר פרופילים אחד, האחראי על ניהול החיבור לפרופיל האחר.
מחבר פרופיל ברירת המחדל
אם יש רק הגדרה אחת של Cross Profile בקובץ הקוד, תוכלו להימנע משימוש ב-Profile Connector משלכם ולהשתמש ב-com.google.android.enterprise.connectedapps.CrossProfileConnector
. כאן
ברירת המחדל אם לא צוין אף סוג.
כשיוצרים את המחבר בין הפרופילים, אפשר לציין כמה אפשרויות ב-builder:
Scheduled Executor Service
כדי לשלוט בשרשורים שנוצרו על ידי ה-SDK, אפשר להשתמש
#setScheduledExecutorService()
,ארגונית
אם יש לכם צרכים ספציפיים בנוגע לקישור הפרופיל, תוכלו להשתמש ב-
#setBinder
. הזה משמש ככל הנראה רק לנאמני מידע של Device Policy.
מחבר פרופיל בהתאמה אישית
כדי לבצע הגדרות אישיות צריך מחבר פרופיל בהתאמה אישית
(באמצעות CustomProfileConnector
) ויהיה צורך בחשבון אם יש צורך
ב-codebase יחיד (לדוגמה, אם יש לכם כמה תהליכים, אנחנו
מומלץ מחבר אחד לכל תהליך).
כשיוצרים 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
.
בקרים לניהול מדיניות המכשירים (DPC)
אם האפליקציה שלך היא Device Policy Controller, עליך לציין מופע של
DpcProfileBinder
מתייחס ל-DeviceAdminReceiver
שלך.
אם אתם מטמיעים מחבר פרופיל משלכם:
@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
עם הערות צריכה להיות יכולה לראות את כל
הספק שנעשה בו שימוש באפליקציה.
שיחות סינכרוניות
ה-SDK של Connected Apps תומך בקריאות סינכרוניות (חסימות) במקרים שבהם אי אפשר להימנע מהן. עם זאת, יש כמה חסרונות לשימוש בשיחות כאלה (למשל, הסכנה שהשיחות ייחסמו למשך זמן רב), ולכן מומלץ להימנע משיחות סינכרוניות כשהדבר אפשרי. במאמר קריאות אסינכרוניות מוסבר איך משתמשים בקריאות אסינכרוניות.
מחזיקי חיבור
אם אתם משתמשים בקריאות סינכרוניות, עליכם לוודא שיש בעל חיבור רשום לפני שמבצעים קריאות בין פרופילים, אחרת תופיע חריגה. מידע נוסף זמין במאמר 'בעלי חיבורים'.
כדי להוסיף בעל חיבור, צריך לבצע קריאה ל-ProfileConnector#addConnectionHolder(Object)
עם אובייקט כלשהו (יכול להיות שהקריאה תתבצע על ידי מופע האובייקט שמבצע את הקריאה בין הפרופילים). כך יירשם שהאובייקט הזה משתמש בחיבור, ויתבצע ניסיון ליצירת חיבור. צריך לבצע את הקריאה הזו לפני שמבצעים שיחות סינכרוניות. זוהי קריאה לא חוסמת, לכן ייתכן
שהחיבור לא יהיה מוכן (או שהוא לא יהיה אפשרי) עד למועד ביצוע
בטלפון, ובמקרה כזה תחול התנהגות הטיפול הרגילה בשגיאות.
אם אין לכם את ההרשאות המתאימות בכמה פרופילים כשאתם קוראים ל-ProfileConnector#addConnectionHolder(Object)
או שאין פרופיל זמין לחיבור, לא תופיע שגיאה אבל פונקציית ה-callback המקושרת לא תופעל אף פעם. אם ההרשאה תוענק מאוחר יותר או שהפרופיל השני יהפוך
אז החיבור יבוצע ואז תתבצע התקשרות חזרה.
לחלופין, ProfileConnector#connect(Object)
היא שיטת חסימה
יוסיף את האובייקט בתור בעל חיבור ותיצור חיבור או
נותנים UnavailableProfileException
. לא ניתן לקרוא לשיטה הזו מ:
ה-thread של ממשק המשתמש.
שיחות אל ProfileConnector#connect(Object)
ושירותים דומים
ProfileConnector#connect
יחזיר אובייקטים שנסגורים אוטומטית,
מסירים את מחזיק החיבור לאחר הסגירה. כך אפשר להשתמש ב-Google Analytics לצורך:
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
) או מקבלת פרמטר של קריאה חוזרת (callback) מסומנת כלא חוסמת. כל שאר השיטות מסומנות כחסימה.
מומלץ להשתמש בקריאות אסינכרוניות. אם אתם חייבים להשתמש בקריאות סינכרוניות, תוכלו לקרוא את המאמר קריאות סינכרוניות.
התקשרות חזרה
הסוג הבסיסי ביותר של קריאה לא חוסמת הוא method של void שמקבל כאחד מהפרמטרים שלו ממשק שמכיל method שצריך לקרוא אליו עם התוצאה. כדי שהממשקים האלה יפעלו עם ה-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
});
אם הממשק הזה מכיל שיטה אחת, שמקבלת אפס או אחד , ניתן להשתמש בו גם בקריאות למספר פרופילים בו-זמנית.
אפשר להעביר מספר בלתי מוגבל של ערכים באמצעות קריאה חוזרת (callback), אבל החיבור יישאר פתוח רק עבור הערך הראשון. במאמר 'מחזקי חיבורים' מוסבר איך להשאיר את החיבור פתוח כדי לקבל ערכים נוספים.
שיטות סינכרוניות עם קריאות חוזרות
תכונה חריגה אחת של שימוש בקריאות חוזרות (callbacks) עם ה-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, ההתנהגות לא תהיה זהה. יש אין ערובה לכך שתוצג קריאה לשיטת ההתקנה לפני "הפעולה הזו מדפיסה שלישי" מודפס. כל שימוש בשיטה שסומנה כאסינכרונית על ידי ה-SDK חייב לא מניחים הנחות לגבי המועד שבו תיקרא השיטה.
קריאות חוזרות (callbacks) פשוטות
'קריאות חוזרות פשוטות' הן צורה מגבילה יותר של קריאה חוזרת, תכונות נוספות לביצוע שיחות בין פרופילים. ממשקים פשוטים חייבים לכלול שיטה אחת, שיכולה לקבל אפס או פרמטר אחד.
כדי לאכוף את הדרישה שממשק קריאה חוזרת יישאר, צריך לציין את הערך simple=true
בהערה @CrossProfileCallback
.
אפשר להשתמש בקריאות פשוטות של קריאה חוזרת (callback) בשיטות שונות כמו .both()
, .suppliers()
,
ואחרים.
בעלי החיבורים
כשמבצעים קריאה אסינכררונית (באמצעות פונקציות חזרה או פונקציות עתידיות), מתווסף מחזיק חיבור בזמן ביצוע הקריאה, והוא מוסר כשמתקבלת חריגה או ערך.
אם צפויה העברה של יותר מתוצאה אחת באמצעות קריאה חוזרת (callback), הוספה ידנית של הקריאה החוזרת בתור בעל החיבור:
MyCallback b = //...
connector.addConnectionHolder(b);
profileMyClass.other().registerListener(b);
// Now the connection will be held open indefinitely, once finished:
connector.removeConnectionHolder(b);
אפשר להשתמש בזה גם עם בלוק try-with-resources:
MyCallback b = //...
try (ProfileConnectionHolder p = connector.addConnectionHolder(b)) {
profileMyClass.other().registerListener(b);
// Other things running while we expect results
}
אם תתבצע שיחה חוזרת או אם נתקשר אליו בעתיד, החיבור יישאר פתוח עד שמתקבלת תוצאה. אם נגיע למסקנה שתוצאה לא תועבר, אמורה להסיר את הקריאה החוזרת או את העתיד כבעלי החיבור:
connector.removeConnectionHolder(myCallback);
connector.removeConnectionHolder(future);
מידע נוסף מופיע במאמר 'מחזיקי חיבור'.
חוזים עתידיים
ה-SDK תומך גם בחוזים עתידיים. הסוג היחיד של Future שנתמך באופן מקורי הוא ListenableFuture
, אבל אפשר להשתמש גם בסוגי Future מותאמים אישית. כדי להשתמש ב-Futures, פשוט מגדירים סוג Future נתמך כסוג ההחזרה של שיטה חוצת-פרופילים, ואז משתמשים בו כרגיל.
ל-future יש את אותה "תכונה יוצאת דופן" כמו ל-callbacks, שבה שיטה סינכרונית שמחזירה עתיד (למשל באמצעות immediateFuture
) תתנהג באופן שונה כשהיא מופעלת בפרופיל הנוכחי לעומת כשהיא מופעלת בפרופיל אחר. כל שימוש
שמסומנת כאסינכרונית על ידי ה-SDK אינה יכולה להניח הנחות לגבי המועד שבו
תיקרא.
שרשורים
לא לחסום את התוצאות בגלל עתיד של פרופילים שונים או קריאה חוזרת (callback) בדף הראשי thread. אם תעשו זאת, בחלק מהמקרים הקוד ייחסם ללא הגבלת זמן. הסיבה לכך היא שגם החיבור לפרופיל השני הוגדרה ב-thread הראשי. הפעולה הזו אף פעם לא תתבצע אם הוא חסום בהמתנה תוצאה חוצת-פרופילים.
זמינות
אפשר להשתמש באפליקציית המאזינים לזמינות כדי לקבל הודעה כשמצב הזמינות
משתנה, ויכול להיות שייעשה שימוש ב-connector.utils().isAvailable
כדי לקבוע אם
הפרופיל זמין לשימוש. לדוגמה:
crossProfileConnector.registerAvailabilityListener(() -> {
if (crossProfileConnector.utils().isAvailable()) {
// Show cross-profile content
} else {
// Hide cross-profile content
}
});
מחזיקי חיבור
בעלי חיבור הם אובייקטים שרירותיים שמתועדים בתור בעלי את העניין בחיבור חוצה-פרופילים שנוצר ויישמר בחיים.
כברירת מחדל, כשמבצעים קריאות אסינכררוניות, מאחז החיבור יתווסף כשהקריאה תתחיל ויוסרו כשתופיע תוצאה או שגיאה.
אפשר גם להוסיף ולהסיר בעלי קישורים באופן ידני כדי לשלוט טוב יותר בחיבור. ניתן להוסיף בעלי חיבור באמצעות
connector.addConnectionHolder
, והוסר באמצעות
connector.removeConnectionHolder
.
כשמוסיפים לפחות בעל חיבור אחד, ה-SDK ינסה לשמור על חיבור. אם לא נוספו בעלי חיבורים, אפשר לסגור את החיבור.
עליכם לשמור על הפניה לכל בעל חיבור שתוסיפו, ולהסיר אותו כשהוא כבר לא רלוונטי.
שיחות סינכרוניות
לפני ביצוע שיחות סינכרוניות, צריך להוסיף בעלים לחיבור. אפשר לעשות זאת באמצעות כל אובייקט, אבל צריך לעקוב אחרי האובייקט הזה כדי שתוכלו להסיר אותו כשלא תצטרכו יותר לבצע קריאות סינכרוניות.
קריאות אסינכרוניות
במהלך ביצוע שיחות אסינכרוניות, בעלי החיבורים ינוהלו באופן אוטומטי כך שהחיבור יהיה פתוח בין השיחה לבין התגובה או השגיאה הראשונה. אם אתם זקוקים לחיבור כדי לשרוד מעבר לכך (למשל, כדי לקבל מספר תשובות באמצעות קריאה חוזרת (callback) יחידה), צריך להוסיף את הקריאה החוזרת עצמה את מאגר הנתונים של החיבור, ולהסיר אותו כשכבר לא תצטרכו לקבל .
טיפול בשגיאות
כברירת מחדל, כל קריאה לפרופיל השני כשהפרופיל השני לא זמין תוביל להשלכת UnavailableProfileException
(או להעברה ל-Future, או ל-error callback עבור קריאה אסינכררונית).
כדי למנוע זאת, מפתחים יכולים להשתמש ב-#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[]>>
הוא
סוג חוקי.
חוזים עתידיים
יש תמיכה בסוגי המשתנים הבאים רק כסוגי החזרים:
com.google.common.util.concurrent.ListenableFuture
רכיבי wrapper מותאמים אישית של Parcelable
אם הסוג לא מופיע ברשימה הקודמת, בדקו קודם אם ניתן
להטמיע את android.os.Parcelable
או java.io.Serializable
בצורה נכונה. אם הוא לא יכול לראות עטיפות שניתן לחלק, הוא יכול להוסיף תמיכה בסוג שלכם.
מעטפות לעתיד בהתאמה אישית
אם אתם רוצים להשתמש בסוג עתידי שלא מופיע ברשימה הקודמת, תוכלו להיעזר במאמר חבילות עטיפה עתידיות כדי להוסיף תמיכה.
אריזות אריזה
רכיבי אריזה שניתנים לחלוקה הם הדרך שבה ערכת ה-SDK מוסיפה תמיכה למשתמשים שאינם ניתנים לחלוקה ואי אפשר לשנות אותם. ה-SDK כולל חבילות עטיפה לסוגים רבים, אבל אם הסוג שבו אתם צריכים להשתמש לא נכלל, תצטרכו לכתוב חבילה משלכם.
Parcelable Wrapper הוא סוג של כיתת עטיפה שמיועדת לעטוף סוג אחר של כיתות ולהפוך אותו ל-Parcelable. הוא פועל לפי חוזה סטטי מוגדר ומשויך ל-SDK, כך שאפשר להשתמש בו כדי להמיר סוג נתון לסוג שניתן לחלוקה, וגם לחלץ את הסוג הזה מהסוג שניתן לחלוקה.
הערה
חובה להוסיף הערה למחלקה של wrapper של החבילה @CustomParcelableWrapper
,
שמציין את המחלקה הארוזה כ-originalType
. לדוגמה:
@CustomParcelableWrapper(originalType=ImmutableList.class)
פורמט
רכיבי wrapper של חבילות חייבים להטמיע את 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
אחרי שיוצרים את החבילה, כדי להשתמש ב-wrapper בהתאמה אישית של מגרש, צריך לרשום אותו עם ה-SDK.
כדי לעשות זאת, צריך לציין את parcelableWrappers={YourParcelableWrapper.class}
באחת מהאפשרויות
הערה מסוג CustomProfileConnector
או הערה מסוג CrossProfile
בכיתה.
Wrappers עתידיים
גרסה עתידית של Wrappers היא האופן שבו ערכת ה-SDK מוסיפה תמיכה בחוזים עתידיים בכל הפרופילים. ה-SDK כולל תמיכה ב-ListenableFuture
כברירת מחדל, אבל אפשר להוסיף תמיכה בעצמכם לסוגים אחרים של Future.
Future Wrapper הוא סוג של כיתת עטיפה שמיועדת לעטוף סוג ספציפי של Future ולהפוך אותו לזמין ל-SDK. היא פועלת לפי חוזה סטטי מוגדר, רשומים ב-SDK.
הערה
יש להוסיף הערות למחלקה wrapper העתידית @CustomFutureWrapper
, תוך ציון
המחלקה הארוזה בתור originalType
. לדוגמה:
@CustomFutureWrapper(originalType=SettableFuture.class)
``` ### Format
Future wrappers must extend
`com.google.android.enterprise.connectedapps.FutureWrapper`.
Future wrappers must have a static `W create(Bundler, BundlerType)` method which
creates an instance of the wrapper. At the same time this should create an
instance of the wrapped future type. This should be returned by a non-static `T`
`getFuture()` method. The `onResult(E)` and `onException(Throwable)` methods
must be implemented to pass the result or throwable to the wrapped future.
Future wrappers must also have a static `void writeFutureResult(Bundler,`
`BundlerType, T, FutureResultWriter<E>)` method. This should register with the
passed in future for results, and when a result is given, call
`resultWriter.onSuccess(value)`. If an exception is given,
`resultWriter.onFailure(exception)` should be called.
Finally, future wrappers must also have a static `T<Map<Profile, E>>`
`groupResults(Map<Profile, T<E>> results)` method which converts a map from
profile to future, into a future of a map from profile to result.
`CrossProfileCallbackMultiMerger` can be used to make this logic easier.
For example:
```java
/** A very simple 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
לאחר יצירת ה-wrapper המותאם אישית העתידי, יהיה עליך לרשום אותו ערכת ה-SDK.
כדי לעשות זאת, צריך לציין את futureWrappers={YourFutureWrapper.class}
הערה CustomProfileConnector
או הערה CrossProfile
בכיתה.
מצב הפעלה ישירה
אם האפליקציה תומכת בהפעלה ישירה מצב , ייתכן שיהיה צורך לבצע שיחות חוצות-פרופילים לפני שהפרופיל יבוטל. כברירת מחדל, ערכת ה-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
ב-builder.
בעקבות השינוי הזה, תקבלו עדכון על הזמינות ותוכלו לבצע שיחות בין הפרופילים גם אם הנעילה של הפרופיל השני לא פתוחה. באחריותך כדי לוודא שהשיחות ניגשות רק לאחסון מוצפן במכשיר.