אפליקציות של תוספים ופקודות מקומיות

ערכת ה-SDK של Android Management API‏ (AMAPI) מאפשרת לאפליקציית תוסף שצוינה על ידי EMM לתקשר ישירות עם Android Device Policy‏ (ADP) ולהפעיל Commands במכשיר.

במאמר שילוב עם AMAPI SDK מוסבר בהרחבה על הספרייה הזו ואיך להוסיף אותה לאפליקציה.

אחרי שילוב ה-SDK, אפליקציית התוסף יכולה לתקשר עם ADP כדי:

הפעלת פקודה

אפליקציית תוסף יכולה לבקש להנפיק פקודות באמצעות ADP. ‫IssueCommandRequest מכיל את אובייקט הבקשה שיכיל פרטים על הפקודה שתוצא ופרמטרים ספציפיים.

ניקוי נתוני האפליקציה

בקטע הקוד הבא אפשר לראות איך לשלוח בקשה למחיקת הנתונים של החבילה:

import android.util.Log;
...
import com.google.android.managementapi.commands.LocalCommandClientFactory;
import com.google.android.managementapi.commands.model.Command;
import com.google.android.managementapi.commands.model.GetCommandRequest;
import com.google.android.managementapi.commands.model.IssueCommandRequest;
import com.google.android.managementapi.commands.model.IssueCommandRequest.ClearAppsData;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;

...
  void issueClearAppDataCommand(ImmutableList<String> packageNames) {
    Futures.addCallback(
        LocalCommandClientFactory.create(getContext())
            .issueCommand(createClearAppRequest(packageNames)),
        new FutureCallback<Command>() {
          @Override
          public void onSuccess(Command result) {
            // Process the returned command result here
            Log.i(TAG, "Successfully issued command");
          }

          @Override
          public void onFailure(Throwable t) {
            Log.e(TAG, "Failed to issue command", t);
          }
        },
        MoreExecutors.directExecutor());
  }

  IssueCommandRequest createClearAppRequest(ImmutableList<String> packageNames) {
    return IssueCommandRequest.builder()
        .setClearAppsData(
            ClearAppsData.builder()
                .setPackageNames(packageNames)
                .build()
        )
        .build();
  }
...

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

איפוס המכשיר או פרופיל העבודה

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

שני הפרמטרים של Wipe הם אופציונליים. אם אתם לא רוצים לציין ערך, אל תשתמשו ב-builder setter או שתעבירו אוסף ריק או מחרוזת ריקה (אל תעבירו את הערך null).

  • WipeDataFlag הערכים קובעים אילו נתונים יימחקו:
    • ‫PRESERVE_RESET_PROTECTION_DATA: שמירת הנתונים של הגנת האיפוס, שנמחקים כברירת מחדל. ההגדרה חלה רק על מכשירים בבעלות החברה.
    • ‫WIPE_EXTERNAL_STORAGE: גם מוחק את האחסון החיצוני, כמו כרטיסי SD. ההגדרה חלה רק על מכשירים בבעלות החברה.
    • ‫WIPE_ESIMS: הסרה של כל כרטיסי ה-eSIM ממכשירים בבעלות החברה. במכשירים בבעלות אישית, רק כרטיסי eSIM מנוהלים יוסרו (ב-Android מגרסה 16 ואילך, כרטיסי eSIM מנוהלים תמיד מוסרים עם פרופיל העבודה, גם אם הדגל הזה מוגדר וגם אם לא).
  • ‫wipeReason: הודעה קצרה, עד 200 תווים, שמוצגת למשתמש כשפרופיל העבודה מוסר ממכשיר בבעלות אישית. אין לה השפעה על מכשירים בבעלות החברה.

בקטע הקוד הבא אפשר לראות איך שולחים בקשה לאיפוס המכשיר או פרופיל העבודה:

Kotlin

import android.content.Context
import android.util.Log
...
import com.google.android.managementapi.commands.CommandException
import com.google.android.managementapi.commands.LocalCommandClientFactory
import com.google.android.managementapi.commands.model.IssueCommandRequest
import com.google.android.managementapi.commands.model.IssueCommandRequest.Wipe
import com.google.android.managementapi.commands.model.IssueCommandRequest.Wipe.WipeDataFlag

...
  suspend fun issueWipeCommand(
      context: Context,
      wipeDataFlags: List<WipeDataFlag>,
      wipeReason: String,
  ) {
    try {
      val command =
          LocalCommandClientFactory.create(context)
              .issueCommandAwait(createWipeRequest(wipeDataFlags, wipeReason))
      Log.i(TAG, "Successfully issued wipe command: ${command.commandId}")
    } catch (e: CommandException) {
      Log.e(TAG, "Failed to issue wipe command", e)
    }
  }

  fun createWipeRequest(
      wipeDataFlags: List<WipeDataFlag>,
      wipeReason: String,
  ): IssueCommandRequest =
      IssueCommandRequest.builder()
          .setWipe(
              Wipe.builder()
                  .setWipeDataFlags(wipeDataFlags)
                  .setWipeReason(wipeReason)
                  .build()
          )
          .build()
...

Java

import android.content.Context;
import android.util.Log;
...
import com.google.android.managementapi.commands.LocalCommandClientFactory;
import com.google.android.managementapi.commands.model.Command;
import com.google.android.managementapi.commands.model.IssueCommandRequest;
import com.google.android.managementapi.commands.model.IssueCommandRequest.Wipe;
import com.google.android.managementapi.commands.model.IssueCommandRequest.Wipe.WipeDataFlag;
import com.google.common.collect.ImmutableList;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;

...
  void issueWipeCommand(
      Context context,
      ImmutableList<WipeDataFlag> wipeDataFlags,
      String wipeReason) {
    Futures.addCallback(
        LocalCommandClientFactory.create(context)
            .issueCommand(createWipeRequest(wipeDataFlags, wipeReason)),
        new FutureCallback<Command>() {
          @Override
          public void onSuccess(Command result) {
            Log.i(TAG, "Successfully issued wipe command: " + result.getCommandId());
          }

          @Override
          public void onFailure(Throwable t) {
            Log.e(TAG, "Failed to issue wipe command", t);
          }
        },
        MoreExecutors.directExecutor());
  }

  IssueCommandRequest createWipeRequest(
      ImmutableList<WipeDataFlag> wipeDataFlags, String wipeReason) {
    return IssueCommandRequest.builder()
        .setWipe(
            Wipe.builder()
                .setWipeDataFlags(wipeDataFlags)
                .setWipeReason(wipeReason)
                .build())
        .build();
  }
...

Get Command

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

import android.util.Log;
...
import com.google.android.managementapi.commands.LocalCommandClientFactory;
...
import com.google.android.managementapi.commands.model.GetCommandRequest;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.MoreExecutors;

...
  void getCommand(String commandId) {
    Futures.addCallback(
        LocalCommandClientFactory.create(getApplication())
            .getCommand(GetCommandRequest.builder().setCommandId(commandId).build()),
        new FutureCallback<Command>() {
          @Override
          public void onSuccess(Command result) {
            // Process the returned command result here
            Log.i(Constants.TAG, "Successfully issued command");
          }

          @Override
          public void onFailure(Throwable t) {
            Log.e(Constants.TAG, "Failed to issue command", t);
          }
        },
        MoreExecutors.directExecutor());
  }
  ...

האזנה לקריאות חוזרות (callbacks) של שינוי סטטוס הפקודה

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

  1. ההתראות על שינויים בסטטוס הפקודה נשלחות אל CommandListener. צריך להטמיע את הממשק הזה באפליקציה ולספק הטמעה של אופן הטיפול בעדכוני הסטטוס שהתקבלו.
  2. מרחיבים את NotificationReceiverService ומספקים מכונה של CommandListener באמצעות השיטה getCommandListener.
  3. מגדירים את מדיניות האפליקציה כדי להקצות את התפקיד COMPANION_APP לאפליקציה (ראו הגדרת מדיניות).

    import com.google.android.managementapi.commands.CommandListener;
    import com.google.android.managementapi.notification.NotificationReceiverService;
    
    ...
    
    public class SampleCommandService extends NotificationReceiverService {
    
     @Override
     public CommandListener getCommandListener() {
       // return the concrete implementation from previous step
       return ...;
     }
    }
    

הגדרת המדיניות

כדי לאפשר לאפליקציה הנלווית לתקשר ישירות עם ADP, צריך להקצות לאפליקציה את התפקיד COMPANION_APP באמצעות השדה roles במדיניות האפליקציה:

 "applications": [{
   "packageName": "com.amapi.extensibility.demo",
   "installType": "FORCE_INSTALLED",
   "roles": [
     { "roleType": "COMPANION_APP" }
   ]
 }]

כדי לאפשר לאפליקציה הנלווית להנפיק פקודת Wipe מקומית, מגדירים את extraCapabilities.localDeviceWipeCommand לערך LOCAL_DEVICE_WIPE_COMMAND_ALLOWED:

 "applications": [{
   "packageName": "com.amapi.extensibility.demo",
   "installType": "FORCE_INSTALLED",
   "roles": [
     {
       "roleType": "COMPANION_APP",
       "extraCapabilities": {
         "localDeviceWipeCommand": "LOCAL_DEVICE_WIPE_COMMAND_ALLOWED"
       }
     }
   ]
 }]

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

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

בדיקה

בדיקות יחידה (unit testing)

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

בדיקת אינטגרציה

כדי לבצע בדיקה עם ADP, צריך את הפרטים הבאים:

  1. שם החבילה של אפליקציית התוסף.
  2. גיבוב SHA-256 בקידוד Base64 של החתימה שמשויכת לחבילת ה-APK של האפליקציה.
  3. אם בודקים את פונקציית ה-callback, צריך לציין את השם המלא של השירות החדש שתומך ב-callback. (השם המלא של CommandService בדוגמה).