נושאים מתקדמים

הקטעים האלה מיועדים למידע בלבד, ואין צורך לקרוא אותם מלמעלה למטה.

שימוש בממשקי API של מסגרות:

ממשקי ה-API האלה יאוחסנו ב-SDK כדי ליצור ממשק API עקבי יותר (למשל, הימנעות מאובייקטים מסוג UserHandle), אבל בינתיים אפשר לבצע קריאה ישירה אליהם.

ההטמעה פשוטה: אם אתם יכולים לבצע אינטראקציה, קדימה. אם לא, אבל אתם יכולים לבקש, תוכלו להציג למשתמש את ההנחיה/הבאנר/הטיפ וכו'. אם המשתמש מסכים לעבור להגדרות, תוכלו ליצור את כוונה הבקשה ולהשתמש ב-Context#startActivity כדי לשלוח את המשתמש לשם. אתם יכולים להשתמש בשידור כדי לזהות מתי היכולת הזו משתנה, או פשוט לבדוק שוב כשהמשתמש יחזור.

כדי לבדוק את זה, צריך לפתוח את TestDPC בפרופיל העבודה, לעבור לתחתית המסך ולבחור להוסיף את שם החבילה לרשימת ההיתרים של האפליקציות המקושרות. הפעולה הזו מחקה את הוספת האפליקציה לרשימת ההיתרים של האדמין.

מילון מונחים

בקטע הזה מפורטים מונחים מרכזיים שקשורים לפיתוח בכמה פרופילים.

הגדרה בין פרופילים

הגדרה של הצגת היומן האישי בפרופיל העבודה מקבצת יחד כיתות קשורות של ספקי הצגת היומן האישי בפרופיל העבודה, ומספקת הגדרה כללית של התכונות האלה. בדרך כלל תהיה הערה אחת מסוג @CrossProfileConfiguration לכל קוד בסיס, אבל באפליקציות מורכבות יכולות להיות כמה הערות כאלה.

Profile Connector

המחבר מנהל את החיבורים בין הפרופילים. בדרך כלל, כל סוג של פרופיל חוצה יצביע על מחבר ספציפי. בכל סוג של פרופיל חוצה באותו הגדרה חייב להיות שימוש באותו מחבר.

סוג הספק: 'פרופיל משתמש'

סוג הספק של הצגת היומן האישי בפרופיל העבודה מקבצת יחד סוגים קשורים של הצגת היומן האישי בפרופיל העבודה.

Mediator

מתווך נמצא בין קוד ברמה גבוהה לקוד ברמה נמוכה, ומחלק את הקריאות לפרופילים הנכונים וממזג את התוצאות. זהו הקוד היחיד שצריך להיות מותאם לפרופיל. זהו רעיון ארכיטקטוני ולא משהו שמובנה ב-SDK.

סוג הפרופיל

סוג חוצה-פרופילים הוא מחלקה או ממשק שמכילים שיטות עם הערה @CrossProfile. הקוד מסוג זה לא צריך להיות מודע לפרופיל, ובאופן אידיאלי הוא צריך לפעול רק על הנתונים המקומיים שלו.

סוגי פרופילים

סוג הפרופיל
היחס הנוכחיהפרופיל הפעיל שבו אנחנו מבצעים את הפעולה.
אחר(אם הוא קיים) הפרופיל שבו לא מריצים את הפקודה.
Personalמשתמש 0, הפרופיל במכשיר שלא ניתן להשבית.
עבודהבדרך כלל משתמש 10, אבל יכול להיות גם מספר גבוה יותר. אפשר להפעיל או להשבית אותו, והוא משמש לאפליקציות ולנתונים של העבודה.
ראשיאפשר להגדיר את האפשרות הזו באפליקציה. הפרופיל שבו מוצגת תצוגה משולבת של שני הפרופילים.
Secondaryאם מוגדרת 'ראשי', 'משני' הוא הפרופיל שאינו ראשי.
ספקהספקים לפרופיל הראשי הם שני הפרופילים, והספקים לפרופיל המשני הם רק הפרופיל המשני עצמו.

מזהה הפרופיל

סיווג שמייצג סוג של פרופיל (אישי או עבודה). הם יחזרו בשיטות שפועלות בכמה פרופילים, וניתן להשתמש בהם כדי להריץ עוד קוד בפרופילים האלה. אפשר לסדר אותם בסדרת int לאחסון נוח.

במדריך הזה מפורטות המבנים המומלצים ליצירת פונקציות יעילות וניתנות לתחזוקה בכל הפרופילים באפליקציה ל-Android.

המרת CrossProfileConnector למשתנה יחיד (singleton)

צריך להשתמש רק במכונה אחת במהלך מחזור החיים של האפליקציה, אחרת תיווצרו חיבורים מקבילים. אפשר לעשות זאת באמצעות מסגרת להזרקת יחסי תלות כמו Dagger, או באמצעות תבנית Singleton קלאסית, בכיתה חדשה או בכיתה קיימת.

להחדיר או להעביר את מופע הפרופיל שנוצר לכיתה שלכם כשאתם מבצעים את הקריאה, במקום ליצור אותו בשיטה

כך תוכלו להעביר את המופע של FakeProfile שנוצר באופן אוטומטי מאוחר יותר בבדיקות היחידה.

שימוש בתבנית של מתווך

התבנית הנפוצה הזו היא להפוך אחד מממשקי ה-API הקיימים (למשל getEvents()) למודע לפרופיל לכל מבצעי הקריאה שלו. במקרה כזה, ה-API הקיים יכול פשוט להפוך למחלקה או לשיטה של 'מתווך' שמכילה את הקריאה החדשה לקוד שנוצר בכמה פרופילים.

כך לא תצטרכו לאלץ כל מבצע קריאה לדעת איך לבצע קריאה בכמה פרופילים – היא פשוט תהפוך לחלק מ-API שלכם.

כדאי לשקול להוסיף הערה לשיטת ממשק כ-@CrossProfile במקום זאת, כדי להימנע מהצורך לחשוף את מחלקות ההטמעה שלכם בספק

הפתרון הזה מתאים מאוד למסגרות של הזרקת יחסי תלות.

אם אתם מקבלים נתונים מבקשות מפרופילים שונים, כדאי לשקול להוסיף שדה שמציין את הפרופיל שממנו הם הגיעו

זוהי שיטה מומלצת, כי יכול להיות שתרצו לדעת את זה בשכבת ממשק המשתמש (למשל, הוספת סמל תג לדברים שקשורים לעבודה). יכול להיות שתצטרכו להשתמש בו גם אם מזהים של נתונים מסוימים כבר לא ייחודיים בלעדיו, כמו שמות חבילות.

הצגת היומן האישי בפרופיל העבודה

בקטע הזה נסביר איך ליצור אינטראקציות משלכם בין פרופילים.

פרופילים ראשיים

רוב הקריאות בדוגמאות במסמך הזה מכילות הוראות מפורשות לגבי הפרופיל שבו צריך להריץ את הקריאה, כולל פרופיל עבודה, פרופיל אישי או שניהם.

בפועל, באפליקציות עם חוויית משתמש משולבת בפרופיל אחד בלבד, סביר להניח שתרצו שההחלטה הזו תלויה בפרופיל שבו אתם מריצים את האפליקציה. לכן יש שיטות נוחות דומות שמביאות בחשבון גם את העניין הזה, כדי למנוע הצטברות של תנאים מותנים לפרופיל מסוג 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 Type חייב להיות מוגדר באמצעות שיטה עם הערה @CrossProfileProvider. השיטות האלה יקראו בכל פעם שתתבצע קריאה בכמה פרופילים, לכן מומלץ להשתמש באובייקטים ייחודיים לכל סוג.

יצרן

לספק חייב להיות קונסטרוקטור ציבורי שלא מקבל אף ארגומנט או ארגומנט Context יחיד.

שיטות של ספקים

שיטות של ספקים לא יכולות לקבל אף ארגומנט או ארגומנט Context יחיד.

הזרקת תלויות

אם אתם משתמשים במסגרת להזרקת יחסי תלות כמו Dagger כדי לנהל את יחסי התלות, מומלץ להשתמש במסגרת הזו כדי ליצור את הסוגים שלכם בפרופיל, כפי שאתם עושים בדרך כלל, ואז להזריק את הסוגים האלה לכיתה של הספק. לאחר מכן, השיטות @CrossProfileProvider יכולות להחזיר את המופעים האלה שהוזנו.

Profile Connector

לכל הגדרה של פרופיל משותף צריך להיות מחבר פרופיל אחד, שאחראי לניהול החיבור לפרופיל השני.

מחבר פרופיל ברירת המחדל

אם יש רק הגדרה אחת של Cross Profile בקובץ הקוד, תוכלו להימנע משימוש ב-Profile Connector משלכם ולהשתמש ב-com.google.android.enterprise.connectedapps.CrossProfileConnector. זו ברירת המחדל שתופעל אם לא תציינו ערך אחר.

כשיוצרים את המחבר בין הפרופילים, אפשר לציין כמה אפשרויות ב-builder:

  • Scheduled Executor Service

    אם רוצים לשלוט בשרשור שנוצר על ידי ה-SDK, משתמשים ב-#setScheduledExecutorService().

  • ארגונית

    אם יש לכם צרכים ספציפיים בנוגע לקישור הפרופיל, תוכלו להשתמש ב-#setBinder. סביר להניח שרק אמצעי הבקרה של מדיניות המכשיר משתמשים באפשרות הזו.

Custom Profile Connector

כדי להגדיר הגדרות מסוימות (באמצעות CustomProfileConnector), תצטרכו מחבר פרופיל מותאם אישית. בנוסף, תצטרכו מחבר כזה אם אתם צריכים כמה מחברים בקוד בסיס אחד (לדוגמה, אם יש לכם כמה תהליכים, מומלץ להשתמש במחבר אחד לכל תהליך).

כשיוצרים ProfileConnector, הוא אמור להיראות כך:

@GeneratedProfileConnector
public interface MyProfileConnector extends ProfileConnector {
  public static MyProfileConnector create(Context context) {
    // Configuration can be specified on the builder
    return GeneratedMyProfileConnector.builder(context).build();
  }
}
  • serviceClassName

    כדי לשנות את שם השירות שנוצר (שצריך להפנות אליו ב-AndroidManifest.xml), משתמשים ב-serviceClassName=.

  • primaryProfile

    כדי לציין את הפרופיל הראשי, משתמשים ב-primaryProfile.

  • availabilityRestrictions

    כדי לשנות את ההגבלות שה-SDK מטיל על החיבורים ועל הזמינות של הפרופיל, משתמשים ב-availabilityRestrictions.

בקרי מדיניות המכשירים

אם האפליקציה שלכם היא אמצעי בקרה של מדיניות המכשיר, עליכם לציין מופע של 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 בתור הסופר-קלאס. אם אתם צריכים שסופר-הקלאס יהיה כיתה אחרת (שגם היא חייבת להיות Subclass של 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. לא ניתן לקרוא לשיטה הזו מ שרשור ממשק המשתמש.

קריאות ל-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 היא שאפשר, באופן טכני, לכתוב שיטה סינכרונית שמשתמשת ב-callback:

public void install(InstallationCompleteListener callback) {
  callback.installationComplete(1);
}

במקרה כזה, למעשה מדובר בשיטה אסינכררונית, למרות הקריאה החוזרת. הקוד הזה יפעל בצורה תקינה:

System.out.println("This prints first");
installer.install(() -> {
        System.out.println("This prints second");
});
System.out.println("This prints third");

עם זאת, כשמפעילים את הפונקציה באמצעות ה-SDK, ההתנהגות שלה משתנה. אין ערובה ששיטת ההתקנה תופעל לפני ההדפסה של "This prints third". כל שימוש בשיטה שסומנה כסינכרונית על ידי ה-SDK חייב להיות ללא הנחות לגבי המועד שבו השיטה תיקרא.

קריאות חוזרות (callbacks) פשוטות

'התקשרויות חוזרות פשוטות' הן סוג מוגבל יותר של התקשרות חוזרת, שמאפשר להשתמש בתכונות נוספות כשמבצעים שיחות בכמה פרופילים. ממשקים פשוטים חייבים לכלול שיטה אחת, שיכולה לקבל אפס או פרמטר אחד.

כדי לאכוף את הדרישה שממשק קריאה חוזרת יישאר, צריך לציין את הערך simple=true בהערה @CrossProfileCallback.

אפשר להשתמש בקריאות חזרה פשוטות בשיטות שונות כמו .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
}

אם נקיים שיחה עם קריאה חוזרת או עם פונקציית future, החיבור יישאר פתוח עד שיוחזר תוצאה. אם נגיע למסקנה שלא תהיה העברה של תוצאה, עלינו להסיר את הקריאה החוזרת או את ה-future כמחזיק החיבור:

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

מידע נוסף זמין במאמר 'בעלי חיבורים'.

חוזים עתידיים

ב-SDK יש תמיכה גם באופן מקורי באופציות עתידיות. הסוג היחיד של עתיד שנתמך באופן מקורי הוא ListenableFuture, אבל אפשר להשתמש גם בסוגי עתיד מותאמים אישית. כדי להשתמש ב-Futures, פשוט מגדירים סוג Future נתמך כסוג ההחזרה של שיטה חוצת-פרופילים, ואז משתמשים בו כרגיל.

ל-future יש את אותה "תכונה יוצאת דופן" כמו ל-callbacks, שבה שיטה סינכרונית שמחזירה עתיד (למשל באמצעות immediateFuture) תתנהג באופן שונה כשהיא מופעלת בפרופיל הנוכחי לעומת כשהיא מופעלת בפרופיל אחר. אסור להניח משהו לגבי מועד הקריאה של שיטות שסומנו כלא אסינכרוניות על ידי ה-SDK.

שרשורים

לא לחסום את התוצאה של פונקציית future או פונקציית קריאה חוזרת (callback) בכמה פרופילים בשרשור הראשי. אם תעשו זאת, בחלק מהמקרים הקוד ייחסם ללא הגבלת זמן. הסיבה לכך היא שהחיבור לפרופיל השני נוצר גם הוא בשרשור הראשי, וזה אף פעם לא יקרה אם הוא נחסם עד לקבלת תוצאה בכמה פרופילים.

זמינות

אפשר להשתמש במאזין הזמינות כדי לקבל הודעה כשמצב הזמינות משתנה, וב-connector.utils().isAvailable כדי לקבוע אם יש פרופיל אחר שזמין לשימוש. לדוגמה:

crossProfileConnector.registerAvailabilityListener(() -> {
  if (crossProfileConnector.utils().isAvailable()) {
    // Show cross-profile content
  } else {
    // Hide cross-profile content
  }
});

בעלי החיבורים

בעלי החיבורים הם אובייקטים שרירותיים שמתועדים כבעלים של העניין בחיבור בין הפרופילים, שנוצר ונשמר.

כברירת מחדל, כשמבצעים קריאות אסינכררוניות, מאחז החיבור יתווסף כשהקריאה תתחיל ויוסרו כשתופיע תוצאה או שגיאה.

אפשר גם להוסיף ולהסיר בעלי קישורים באופן ידני כדי לשלוט טוב יותר בקישור. אפשר להוסיף בעלי קישורים באמצעות connector.addConnectionHolder ולהסיר אותם באמצעות connector.removeConnectionHolder.

כשמוסיפים לפחות בעל חיבור אחד, ה-SDK ינסה לשמור על חיבור. אם לא נוספו בעלי חיבורים, אפשר לסגור את החיבור.

עליכם לשמור על הפניה לכל בעל חיבור שתוסיפו, ולהסיר אותו כשהוא כבר לא רלוונטי.

קריאות סינכרוניות

לפני שמבצעים שיחות סינכרוניות, צריך להוסיף בעל חיבור. אפשר לעשות זאת באמצעות כל אובייקט, אבל צריך לעקוב אחרי האובייקט הזה כדי שתוכלו להסיר אותו כשלא תצטרכו יותר לבצע קריאות סינכרוניות.

קריאות אסינכרוניות

כשמבצעים קריאות אסינכרוניות, בעלי החיבורים ינוהלו באופן אוטומטי כדי שהחיבור יהיה פתוח בין הקריאה לתגובה או לשגיאה הראשונה. אם אתם צריכים שהחיבור יישאר פעיל אחרי זה (למשל, כדי לקבל מספר תשובות באמצעות קריאה חוזרת אחת), צריך להוסיף את הקריאה החוזרת עצמה כמחזיק החיבור ולהסיר אותה כשלא צריך יותר לקבל נתונים נוספים.

טיפול בשגיאות

כברירת מחדל, כל קריאה לפרופיל השני כשהפרופיל השני לא זמין תוביל להשלכת UnavailableProfileException (או להעברה ל-Future, או ל-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. אם הוא לא יכול לראות עטיפות שניתן לחלק, הוא לא יוכל להוסיף תמיכה בסוג שלכם.

רכיבי wrapper עתידיים בהתאמה אישית

אם אתם רוצים להשתמש בסוג עתידי שלא מופיע ברשימה הקודמת, תוכלו להיעזר במאמר חבילות עטיפה עתידיות כדי להוסיף תמיכה.

חבילות (wrappers) שניתן לחלק

Wrappers של Parcelable הם הדרך שבה ה-SDK מוסיף תמיכה בסוגי נתונים שלא ניתן לחלק (parcel) ולא ניתן לשנות אותם. ערכת ה-SDK כוללת חבילות עטיפה לסוגים רבים, אבל אם הסוג שבו אתם צריכים להשתמש לא נכלל, תצטרכו לכתוב חבילה משלכם.

Parcelable Wrapper הוא סוג של כיתת עטיפה שמיועדת לעטוף סוג אחר של כיתות ולהפוך אותו ל-Parcelable. הוא פועל לפי חוזה סטטי מוגדר ומשויך ל-SDK, כך שאפשר להשתמש בו כדי להמיר סוג נתון לסוג שניתן לחלוקה, וגם לחלץ את הסוג הזה מהסוג שניתן לחלוקה.

הערה

צריך להוסיף הערה @CustomParcelableWrapper לכיתה המעטפת של parcelable, ולהגדיר את הכיתה שעטופה כ-originalType. לדוגמה:

@CustomParcelableWrapper(originalType=ImmutableList.class)

פורמט

חבילות האריזה של Parcelable חייבות להטמיע את Parcelable בצורה נכונה, וחייבת להיות להן שיטה סטטית W of(Bundler, BundlerType, T) שמאריכה את הסוג המארוז, ושיטה T get() לא סטטית שמחזירה את הסוג המארוז.

ה-SDK ישתמש בשיטות האלה כדי לספק תמיכה חלקה בסוג.

Bundler

כדי לאפשר גיבוב של סוגים כלליים (כמו רשימות ומפות), שיטת of מעבירה Bundler שיכול לקרוא (באמצעות #readFromParcel) ולכתוב (באמצעות #writeToParcel) את כל הסוגים הנתמכים ב-Parcel, ו-BundlerType שמייצג את הסוג המוצהר שצריך לכתוב.

המופעים של Bundler ו-BundlerType הם עצמם ניתנים לחלוקה, וצריך לכתוב אותם כחלק מהחלוקה של המעטפת לחלוקה, כדי שאפשר יהיה להשתמש בהם בזמן שחזור המעטפת לחלוקה.

אם BundlerType מייצג סוג כללי, אפשר למצוא את משתני הסוג באמצעות קריאה ל-.typeArguments(). כל ארגומנט מסוג הוא בעצמו BundlerType.

לדוגמה, ParcelableCustomWrapper:

public class CustomWrapper<F> {
  private final F value;

  public CustomWrapper(F value) {
    this.value = value;
  }
  public F value() {
    return value;
  }
}

@CustomParcelableWrapper(originalType = CustomWrapper.class)
public class ParcelableCustomWrapper<E> implements Parcelable {

  private static final int NULL = -1;
  private static final int NOT_NULL = 1;

  private final Bundler bundler;
  private final BundlerType type;
  private final CustomWrapper<E> customWrapper;

  /**
  *   Create a wrapper for a given {@link CustomWrapper}.
  *
  *   <p>The passed in {@link Bundler} must be capable of bundling {@code F}.
  */
  public static <F> ParcelableCustomWrapper<F> of(
      Bundler bundler, BundlerType type, CustomWrapper<F> customWrapper) {
    return new ParcelableCustomWrapper<>(bundler, type, customWrapper);
  }

  public CustomWrapper<E> get() {
    return customWrapper;
  }

  private ParcelableCustomWrapper(
      Bundler bundler, BundlerType type, CustomWrapper<E> customWrapper) {
    if (bundler == null || type == null) {
      throw new NullPointerException();
    }
    this.bundler = bundler;
    this.type = type;
    this.customWrapper = customWrapper;
  }

  private ParcelableCustomWrapper(Parcel in) {
    bundler = in.readParcelable(Bundler.class.getClassLoader());

    int presentValue = in.readInt();

    if (presentValue == NULL) {
      type = null;
      customWrapper = null;
      return;
    }

    type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader());
    BundlerType valueType = type.typeArguments().get(0);

    @SuppressWarnings("unchecked")
    E value = (E) bundler.readFromParcel(in, valueType);

    customWrapper = new CustomWrapper<>(value);
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    dest.writeParcelable(bundler, flags);

    if (customWrapper == null) {
      dest.writeInt(NULL);
      return;
    }

    dest.writeInt(NOT_NULL);
    dest.writeParcelable(type, flags);
    BundlerType valueType = type.typeArguments().get(0);
    bundler.writeToParcel(dest, customWrapper.value(), valueType, flags);
  }

  @Override
  public int describeContents() {
    return 0;
  }

  @SuppressWarnings("rawtypes")
  public static final Creator<ParcelableCustomWrapper> CREATOR =
    new Creator<ParcelableCustomWrapper>() {
      @Override
      public ParcelableCustomWrapper createFromParcel(Parcel in) {
        return new ParcelableCustomWrapper(in);
      }

      @Override
      public ParcelableCustomWrapper[] newArray(int size) {
        return new ParcelableCustomWrapper[size];
      }
    };
}

רישום ב-SDK

אחרי שיוצרים את העטיפה בהתאמה אישית של Parcelable, צריך לרשום אותה ב-SDK.

כדי לעשות זאת, מציינים את הערך parcelableWrappers={YourParcelableWrapper.class} בהערה מסוג CustomProfileConnector או בהערה מסוג CrossProfile בכיתה.

חבילות עתידיות

באמצעות Future Wrappers, ה-SDK מוסיף תמיכה ב-Futures בפרופילים שונים. ה-SDK כולל תמיכה ב-ListenableFuture כברירת מחדל, אבל אפשר להוסיף תמיכה בעצמכם לסוגים אחרים של Future.

Future Wrapper הוא סוג של כיתת עטיפה שמיועדת לעטוף סוג ספציפי של Future ולהפוך אותו לזמין ל-SDK. הוא פועל לפי חוזה סטטי מוגדר, וצריך לרשום אותו ב-SDK.

הערה

צריך להוסיף הערה @CustomFutureWrapper לכיתה העטיפה העתידית, ולציין את הכיתה שעטופה כ-originalType. לדוגמה:

@CustomFutureWrapper(originalType=SettableFuture.class)

פורמט

חבילות עטיפה עתידיות חייבות להרחיב את com.google.android.enterprise.connectedapps.FutureWrapper.

בעתיד, למעטפות צריכה להיות שיטה סטטית W create(Bundler, BundlerType) שיוצרת מופע של המעטפה. באותו הזמן, הפונקציה אמורה ליצור מכונה של סוג ה-Future המכווץ. צריך להחזיר את הערך הזה באמצעות שיטה T getFuture() לא סטטית. צריך להטמיע את השיטות onResult(E) ו-onException(Throwable) כדי להעביר את התוצאה או את ה-throwable ל-Future המכווץ.

גם לעטיפות עתידיות צריכה להיות שיטה סטטית void writeFutureResult(Bundler, BundlerType, T, FutureResultWriter<E>). צריך לרשום את זה עם הערך שיעבור בעתיד לתוצאות, וכאשר תתקבל תוצאה, צריך לבצע קריאה ל-resultWriter.onSuccess(value). אם מוצגת חריגה, צריך להפעיל את resultWriter.onFailure(exception).

לבסוף, למעטפות עתידיות צריכה להיות גם שיטה סטטית T<Map<Profile, E>> groupResults(Map<Profile, T<E>> results) שממירה מפה מפרופיל לעתיד, למפה עתידית מפרופיל לתוצאה. אפשר להשתמש ב-CrossProfileCallbackMultiMerger כדי לפשט את הלוגיקה הזו.

לדוגמה:

/** A basic implementation of the future pattern used to test custom future
wrappers. */
public class SimpleFuture<E> {
  public static interface Consumer<E> {
    void accept(E value);
  }
  private E value;
  private Throwable thrown;
  private final CountDownLatch countDownLatch = new CountDownLatch(1);
  private Consumer<E> callback;
  private Consumer<Throwable> exceptionCallback;

  public void set(E value) {
    this.value = value;
    countDownLatch.countDown();
    if (callback != null) {
      callback.accept(value);
    }
  }

  public void setException(Throwable t) {
    this.thrown = t;
    countDownLatch.countDown();
    if (exceptionCallback != null) {
      exceptionCallback.accept(thrown);
    }
  }

  public E get() {
    try {
      countDownLatch.await();
    } catch (InterruptedException e) {
      eturn null;
    }
    if (thrown != null) {
      throw new RuntimeException(thrown);
    }
    return value;
  }

  public void setCallback(Consumer<E> callback, Consumer<Throwable>
exceptionCallback) {
    if (value != null) {
      callback.accept(value);
    } else if (thrown != null) {
      exceptionCallback.accept(thrown);
    } else {
      this.callback = callback;
      this.exceptionCallback = exceptionCallback;
    }
  }
}
/** Wrapper for adding support for {@link SimpleFuture} to the Connected Apps SDK.
*/
@CustomFutureWrapper(originalType = SimpleFuture.class)
public final class SimpleFutureWrapper<E> extends FutureWrapper<E> {

  private final SimpleFuture<E> future = new SimpleFuture<>();

  public static <E> SimpleFutureWrapper<E> create(Bundler bundler, BundlerType
bundlerType) {
    return new SimpleFutureWrapper<>(bundler, bundlerType);
  }

  private SimpleFutureWrapper(Bundler bundler, BundlerType bundlerType) {
    super(bundler, bundlerType);
  }

  public SimpleFuture<E> getFuture() {
    return future;
  }

  @Override
  public void onResult(E result) {
    future.set(result);
  }

  @Override
  public void onException(Throwable throwable) {
    future.setException(throwable);
  }

  public static <E> void writeFutureResult(
      SimpleFuture<E> future, FutureResultWriter<E> resultWriter) {

    future.setCallback(resultWriter::onSuccess, resultWriter::onFailure);
  }

  public static <E> SimpleFuture<Map<Profile, E>> groupResults(
      Map<Profile, SimpleFuture<E>> results) {
    SimpleFuture<Map<Profile, E>> m = new SimpleFuture<>();

    CrossProfileCallbackMultiMerger<E> merger =
        new CrossProfileCallbackMultiMerger<>(results.size(), m::set);
    for (Map.Entry<Profile, SimpleFuture<E>> result : results.entrySet()) {
      result
        .getValue()
        .setCallback(
          (value) -> merger.onResult(result.getKey(), value),
          (throwable) -> merger.missingResult(result.getKey()));
    }
    return m;
  }
}

רישום ב-SDK

אחרי שיוצרים את העטיפה בהתאמה אישית לעתיד, צריך לרשום אותה ב-SDK כדי להשתמש בה.

כדי לעשות זאת, מציינים את הערך 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.

בעקבות השינוי הזה, תקבלו עדכון על הזמינות ותוכלו לבצע שיחות בין הפרופילים גם אם הנעילה של הפרופיל השני לא פתוחה. באחריותכם לוודא שהשיחות שלכם יגיעו רק לאחסון המוצפן במכשיר.