Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4ff2a4e6 authored by Kathy Chen's avatar Kathy Chen Committed by Android (Google) Code Review
Browse files

Merge "Add query API for apps to check the event status. The detection service...

Merge "Add query API for apps to check the event status. The detection service sends back the response with a status. Add client API for apps to start the consent activity."
parents d8ef185f f8f605c4
Loading
Loading
Loading
Loading
+39 −25
Original line number Diff line number Diff line
@@ -1359,14 +1359,13 @@ package android.app.ambientcontext {
    method @NonNull public android.app.ambientcontext.AmbientContextEventRequest.Builder setOptions(@NonNull android.os.PersistableBundle);
  }
  public final class AmbientContextEventResponse implements android.os.Parcelable {
    method public int describeContents();
    method @Nullable public android.app.PendingIntent getActionPendingIntent();
    method @NonNull public java.util.List<android.app.ambientcontext.AmbientContextEvent> getEvents();
    method @NonNull public String getPackageName();
    method public int getStatusCode();
    method public void writeToParcel(@NonNull android.os.Parcel, int);
    field @NonNull public static final android.os.Parcelable.Creator<android.app.ambientcontext.AmbientContextEventResponse> CREATOR;
  public final class AmbientContextManager {
    method @NonNull public static java.util.List<android.app.ambientcontext.AmbientContextEvent> getEventsFromIntent(@NonNull android.content.Intent);
    method @RequiresPermission(android.Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT) public void queryAmbientContextServiceStatus(@NonNull java.util.Set<java.lang.Integer>, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
    method @RequiresPermission(android.Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT) public void registerObserver(@NonNull android.app.ambientcontext.AmbientContextEventRequest, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>);
    method @RequiresPermission(android.Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT) public void startConsentActivity(@NonNull java.util.Set<java.lang.Integer>);
    method @RequiresPermission(android.Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT) public void unregisterObserver();
    field public static final String EXTRA_AMBIENT_CONTEXT_EVENTS = "android.app.ambientcontext.extra.AMBIENT_CONTEXT_EVENTS";
    field public static final int STATUS_ACCESS_DENIED = 5; // 0x5
    field public static final int STATUS_MICROPHONE_DISABLED = 4; // 0x4
    field public static final int STATUS_NOT_SUPPORTED = 2; // 0x2
@@ -1375,22 +1374,6 @@ package android.app.ambientcontext {
    field public static final int STATUS_UNKNOWN = 0; // 0x0
  }
  public static final class AmbientContextEventResponse.Builder {
    ctor public AmbientContextEventResponse.Builder();
    method @NonNull public android.app.ambientcontext.AmbientContextEventResponse.Builder addEvent(@NonNull android.app.ambientcontext.AmbientContextEvent);
    method @NonNull public android.app.ambientcontext.AmbientContextEventResponse build();
    method @NonNull public android.app.ambientcontext.AmbientContextEventResponse.Builder setActionPendingIntent(@NonNull android.app.PendingIntent);
    method @NonNull public android.app.ambientcontext.AmbientContextEventResponse.Builder setPackageName(@NonNull String);
    method @NonNull public android.app.ambientcontext.AmbientContextEventResponse.Builder setStatusCode(int);
  }
  public final class AmbientContextManager {
    method @Nullable public static android.app.ambientcontext.AmbientContextEventResponse getResponseFromIntent(@NonNull android.content.Intent);
    method @RequiresPermission(android.Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT) public void registerObserver(@NonNull android.app.ambientcontext.AmbientContextEventRequest, @NonNull android.app.PendingIntent);
    method @RequiresPermission(android.Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT) public void unregisterObserver();
    field public static final String EXTRA_AMBIENT_CONTEXT_EVENT_RESPONSE = "android.app.ambientcontext.extra.AMBIENT_CONTEXT_EVENT_RESPONSE";
  }
}
package android.app.assist {
@@ -10704,14 +10687,45 @@ package android.security.keystore.recovery {
package android.service.ambientcontext {
  public final class AmbientContextDetectionResult implements android.os.Parcelable {
    method public int describeContents();
    method @NonNull public java.util.List<android.app.ambientcontext.AmbientContextEvent> getEvents();
    method @NonNull public String getPackageName();
    method public void writeToParcel(@NonNull android.os.Parcel, int);
    field @NonNull public static final android.os.Parcelable.Creator<android.service.ambientcontext.AmbientContextDetectionResult> CREATOR;
  }
  public static final class AmbientContextDetectionResult.Builder {
    ctor public AmbientContextDetectionResult.Builder();
    method @NonNull public android.service.ambientcontext.AmbientContextDetectionResult.Builder addEvent(@NonNull android.app.ambientcontext.AmbientContextEvent);
    method @NonNull public android.service.ambientcontext.AmbientContextDetectionResult build();
    method @NonNull public android.service.ambientcontext.AmbientContextDetectionResult.Builder setPackageName(@NonNull String);
  }
  public abstract class AmbientContextDetectionService extends android.app.Service {
    ctor public AmbientContextDetectionService();
    method @Nullable public final android.os.IBinder onBind(@NonNull android.content.Intent);
    method public abstract void onStartDetection(@NonNull android.app.ambientcontext.AmbientContextEventRequest, @NonNull String, @NonNull java.util.function.Consumer<android.app.ambientcontext.AmbientContextEventResponse>);
    method @BinderThread public abstract void onQueryServiceStatus(@NonNull int[], @NonNull String, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionServiceStatus>);
    method @BinderThread public abstract void onStartDetection(@NonNull android.app.ambientcontext.AmbientContextEventRequest, @NonNull String, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionResult>, @NonNull java.util.function.Consumer<android.service.ambientcontext.AmbientContextDetectionServiceStatus>);
    method public abstract void onStopDetection(@NonNull String);
    field public static final String SERVICE_INTERFACE = "android.service.ambientcontext.AmbientContextDetectionService";
  }
  public final class AmbientContextDetectionServiceStatus implements android.os.Parcelable {
    method public int describeContents();
    method @NonNull public String getPackageName();
    method public int getStatusCode();
    method public void writeToParcel(@NonNull android.os.Parcel, int);
    field @NonNull public static final android.os.Parcelable.Creator<android.service.ambientcontext.AmbientContextDetectionServiceStatus> CREATOR;
  }
  public static final class AmbientContextDetectionServiceStatus.Builder {
    ctor public AmbientContextDetectionServiceStatus.Builder();
    method @NonNull public android.service.ambientcontext.AmbientContextDetectionServiceStatus build();
    method @NonNull public android.service.ambientcontext.AmbientContextDetectionServiceStatus.Builder setPackageName(@NonNull String);
    method @NonNull public android.service.ambientcontext.AmbientContextDetectionServiceStatus.Builder setStatusCode(int);
  }
}
package android.service.appprediction {
+3 −3
Original line number Diff line number Diff line
@@ -25,7 +25,7 @@ import android.app.ContextImpl.ServiceInitializationState;
import android.app.admin.DevicePolicyManager;
import android.app.admin.IDevicePolicyManager;
import android.app.ambientcontext.AmbientContextManager;
import android.app.ambientcontext.IAmbientContextEventObserver;
import android.app.ambientcontext.IAmbientContextManager;
import android.app.appsearch.AppSearchManagerFrameworkInitializer;
import android.app.blob.BlobStoreManagerFrameworkInitializer;
import android.app.cloudsearch.CloudSearchManager;
@@ -1542,8 +1542,8 @@ public final class SystemServiceRegistry {
                            throws ServiceNotFoundException {
                        IBinder iBinder = ServiceManager.getServiceOrThrow(
                                Context.AMBIENT_CONTEXT_SERVICE);
                        IAmbientContextEventObserver manager =
                                IAmbientContextEventObserver.Stub.asInterface(iBinder);
                        IAmbientContextManager manager =
                                IAmbientContextManager.Stub.asInterface(iBinder);
                        return new AmbientContextManager(ctx.getOuterContext(), manager);
                    }});

+17 −8
Original line number Diff line number Diff line
@@ -23,6 +23,9 @@ import android.os.Parcelable;
import android.os.PersistableBundle;
import android.util.ArraySet;

import com.android.internal.util.AnnotationValidations;
import com.android.internal.util.Preconditions;

import java.util.HashSet;
import java.util.Set;

@@ -36,15 +39,17 @@ public final class AmbientContextEventRequest implements Parcelable {
    @NonNull private final Set<Integer> mEventTypes;
    @NonNull private final PersistableBundle mOptions;

    AmbientContextEventRequest(
    private AmbientContextEventRequest(
            @NonNull Set<Integer> eventTypes,
            @NonNull PersistableBundle options) {
        this.mEventTypes = eventTypes;
        com.android.internal.util.AnnotationValidations.validate(
                NonNull.class, null, mEventTypes);
        AnnotationValidations.validate(NonNull.class, null, mEventTypes);
        Preconditions.checkArgument(!eventTypes.isEmpty(), "eventTypes cannot be empty");
        for (int eventType : eventTypes) {
            AnnotationValidations.validate(AmbientContextEvent.EventCode.class, null, eventType);
        }
        this.mOptions = options;
        com.android.internal.util.AnnotationValidations.validate(
                NonNull.class, null, mOptions);
        AnnotationValidations.validate(NonNull.class, null, mOptions);
    }

    /**
@@ -80,16 +85,20 @@ public final class AmbientContextEventRequest implements Parcelable {

    /** @hide */
    @SuppressWarnings({"unchecked", "RedundantCast"})
    AmbientContextEventRequest(@NonNull Parcel in) {
    private AmbientContextEventRequest(@NonNull Parcel in) {
        Set<Integer> eventTypes = (Set<Integer>) in.readArraySet(Integer.class.getClassLoader());
        PersistableBundle options = (PersistableBundle) in.readTypedObject(
                PersistableBundle.CREATOR);

        this.mEventTypes = eventTypes;
        com.android.internal.util.AnnotationValidations.validate(
        AnnotationValidations.validate(
                NonNull.class, null, mEventTypes);
        Preconditions.checkArgument(!eventTypes.isEmpty(), "eventTypes cannot be empty");
        for (int eventType : eventTypes) {
            AnnotationValidations.validate(AmbientContextEvent.EventCode.class, null, eventType);
        }
        this.mOptions = options;
        com.android.internal.util.AnnotationValidations.validate(
        AnnotationValidations.validate(
                NonNull.class, null, mOptions);
    }

+223 −59
Original line number Diff line number Diff line
@@ -17,116 +17,280 @@
package android.app.ambientcontext;

import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.RemoteCallback;
import android.os.RemoteException;

import com.android.internal.util.Preconditions;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.function.Consumer;

/**
 * Allows granted apps to register for particular pre-defined {@link AmbientContextEvent}s.
 * After successful registration, the app receives a callback on the provided {@link PendingIntent}
 * when the requested event is detected.
 * <p />
 *
 * Example:
 *
 * <pre><code>
 *     // Create request
 *     AmbientContextEventRequest request = new AmbientContextEventRequest.Builder()
 *         .addEventType(AmbientContextEvent.EVENT_COUGH)
 *         .addEventTYpe(AmbientContextEvent.EVENT_SNORE)
 *         .build();
 *     // Create PendingIntent
 *     Intent intent = new Intent(actionString, null, context, MyBroadcastReceiver.class)
 *         .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
 *     PendingIntent pendingIntent = PendingIntents.getBroadcastMutable(context, 0, intent, 0);
 *     // Register for events
 *     AmbientContextManager ambientContextManager =
 *         context.getSystemService(AmbientContextManager.class);
 *    ambientContextManager.registerObserver(request, pendingIntent);
 *
 *    // Handle the callback intent in your receiver
 *    {@literal @}Override
 *    protected void onReceive(Context context, Intent intent) {
 *      AmbientContextEventResponse response =
 *          AmbientContextManager.getResponseFromIntent(intent);
 *      if (response != null) {
 *        if (response.getStatusCode() == AmbientContextEventResponse.STATUS_SUCCESS) {
 *          // Do something useful with response.getEvent()
 *        } else if (response.getStatusCode() == AmbientContextEventResponse.STATUS_ACCESS_DENIED) {
 *          // Redirect users to grant access
 *          PendingIntent callbackPendingIntent = response.getCallbackPendingIntent();
 *          if (callbackPendingIntent != null) {
 *            callbackPendingIntent.send();
 *          }
 *        } else ...
 *      }
 *    }
 * </code></pre>
 * Allows granted apps to register for event types defined in {@link AmbientContextEvent}.
 * After registration, the app receives a Consumer callback of the service status.
 * If it is {@link STATUS_SUCCESSFUL}, when the requested events are detected, the provided
 * {@link PendingIntent} callback will receive the list of detected {@link AmbientContextEvent}s.
 * If it is {@link STATUS_ACCESS_DENIED}, the app can call {@link #startConsentActivity}
 * to load the consent screen.
 *
 * @hide
 */
@SystemApi
@SystemService(Context.AMBIENT_CONTEXT_SERVICE)
public final class AmbientContextManager {
    /**
     * The bundle key for the service status query result, used in
     * {@code RemoteCallback#sendResult}.
     *
     * @hide
     */
    public static final String STATUS_RESPONSE_BUNDLE_KEY =
            "android.app.ambientcontext.AmbientContextStatusBundleKey";

    /**
     * The key of an Intent extra indicating the response.
     * The key of an intent extra indicating a list of detected {@link AmbientContextEvent}s.
     * The intent is sent to the app in the app's registered {@link PendingIntent}.
     */
    public static final String EXTRA_AMBIENT_CONTEXT_EVENT_RESPONSE =
            "android.app.ambientcontext.extra.AMBIENT_CONTEXT_EVENT_RESPONSE";
    public static final String EXTRA_AMBIENT_CONTEXT_EVENTS =
            "android.app.ambientcontext.extra.AMBIENT_CONTEXT_EVENTS";

    /**
     * Allows clients to retrieve the response from the intent.
     * An unknown status.
     */
    public static final int STATUS_UNKNOWN = 0;

    /**
     * The value of the status code that indicates success.
     */
    public static final int STATUS_SUCCESS = 1;

    /**
     * The value of the status code that indicates one or more of the
     * requested events are not supported.
     */
    public static final int STATUS_NOT_SUPPORTED = 2;

    /**
     * The value of the status code that indicates service not available.
     */
    public static final int STATUS_SERVICE_UNAVAILABLE = 3;

    /**
     * The value of the status code that microphone is disabled.
     */
    public static final int STATUS_MICROPHONE_DISABLED = 4;

    /**
     * The value of the status code that the app is not granted access.
     */
    public static final int STATUS_ACCESS_DENIED = 5;

    /** @hide */
    @IntDef(prefix = { "STATUS_" }, value = {
            STATUS_UNKNOWN,
            STATUS_SUCCESS,
            STATUS_NOT_SUPPORTED,
            STATUS_SERVICE_UNAVAILABLE,
            STATUS_MICROPHONE_DISABLED,
            STATUS_ACCESS_DENIED
    }) public @interface StatusCode {}

    /**
     * Allows clients to retrieve the list of {@link AmbientContextEvent}s from the intent.
     *
     * @param intent received from the PendingIntent callback
     *
     * @return the AmbientContextEventResponse, or null if not present
     * @return the list of events, or an empty list if the intent doesn't have such events.
     */
    @Nullable
    public static AmbientContextEventResponse getResponseFromIntent(
            @NonNull Intent intent) {
        if (intent.hasExtra(AmbientContextManager.EXTRA_AMBIENT_CONTEXT_EVENT_RESPONSE)) {
            return intent.getParcelableExtra(EXTRA_AMBIENT_CONTEXT_EVENT_RESPONSE);
    @NonNull public static List<AmbientContextEvent> getEventsFromIntent(@NonNull Intent intent) {
        if (intent.hasExtra(AmbientContextManager.EXTRA_AMBIENT_CONTEXT_EVENTS)) {
            return intent.getParcelableArrayListExtra(EXTRA_AMBIENT_CONTEXT_EVENTS);
        } else {
            return null;
            return new ArrayList<>();
        }
    }

    private final Context mContext;
    private final IAmbientContextEventObserver mService;
    private final IAmbientContextManager mService;

    /**
     * {@hide}
     */
    public AmbientContextManager(Context context, IAmbientContextEventObserver service) {
    public AmbientContextManager(Context context, IAmbientContextManager service) {
        mContext = context;
        mService = service;
    }

    /**
     * Queries the {@link AmbientContextEvent} service status for the calling package, and
     * sends the result to the {@link Consumer} right after the call. This is used by foreground
     * apps to check whether the requested events are enabled for detection on the device.
     * If all events are enabled for detection, the response has
     * {@link AmbientContextManager#STATUS_SUCCESS}.
     * If any of the events are not consented by user, the response has
     * {@link AmbientContextManager#STATUS_ACCESS_DENIED}, and the app can
     * call {@link #startConsentActivity} to redirect the user to the consent screen.
     * <p />
     *
     * Example:
     *
     * <pre><code>
     *   Set<Integer> eventTypes = new HashSet<>();
     *   eventTypes.add(AmbientContextEvent.EVENT_COUGH);
     *   eventTypes.add(AmbientContextEvent.EVENT_SNORE);
     *
     *   // Create Consumer
     *   Consumer<Integer> statusConsumer = response -> {
     *     int status = status.getStatusCode();
     *     if (status == AmbientContextManager.STATUS_SUCCESS) {
     *       // Show user it's enabled
     *     } else if (status == AmbientContextManager.STATUS_ACCESS_DENIED) {
     *       // Send user to grant access
     *       startConsentActivity(eventTypes);
     *     }
     *   };
     *
     *   // Query status
     *   AmbientContextManager ambientContextManager =
     *       context.getSystemService(AmbientContextManager.class);
     *   ambientContextManager.queryAmbientContextStatus(eventTypes, executor, statusConsumer);
     * </code></pre>
     *
     * @param eventTypes The set of event codes to check status on.
     * @param executor Executor on which to run the consumer callback.
     * @param consumer The consumer that handles the status code.
     */
    @RequiresPermission(Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT)
    public void queryAmbientContextServiceStatus(
            @NonNull @AmbientContextEvent.EventCode Set<Integer> eventTypes,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull @StatusCode Consumer<Integer> consumer) {
        try {
            RemoteCallback callback = new RemoteCallback(result -> {
                int status = result.getInt(STATUS_RESPONSE_BUNDLE_KEY);
                final long identity = Binder.clearCallingIdentity();
                try {
                    executor.execute(() -> consumer.accept(status));
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            });
            mService.queryServiceStatus(integerSetToIntArray(eventTypes),
                    mContext.getOpPackageName(), callback);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
     * Requests the consent data host to open an activity that allows users to modify consent.
     *
     * @param eventTypes The set of event codes to be consented.
     */
    @RequiresPermission(Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT)
    public void startConsentActivity(
            @NonNull @AmbientContextEvent.EventCode Set<Integer> eventTypes) {
        try {
            mService.startConsentActivity(
                    integerSetToIntArray(eventTypes), mContext.getOpPackageName());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    @NonNull
    private static int[] integerSetToIntArray(@NonNull Set<Integer> integerSet) {
        int[] intArray = new int[integerSet.size()];
        int i = 0;
        for (Integer type : integerSet) {
            intArray[i++] = type;
        }
        return intArray;
    }

    /**
     * Allows app to register as a {@link AmbientContextEvent} observer. The
     * observer receives a callback on the provided {@link PendingIntent} when the requested
     * event is detected. Registering another observer from the same package that has already been
     * registered will override the previous observer.
     * <p />
     *
     * Example:
     *
     * <pre><code>
     *   // Create request
     *   AmbientContextEventRequest request = new AmbientContextEventRequest.Builder()
     *       .addEventType(AmbientContextEvent.EVENT_COUGH)
     *       .addEventType(AmbientContextEvent.EVENT_SNORE)
     *       .build();
     *
     *   // Create PendingIntent for delivering detection results to my receiver
     *   Intent intent = new Intent(actionString, null, context, MyBroadcastReceiver.class)
     *       .addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
     *   PendingIntent pendingIntent =
     *       PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
     *
     *   // Create Consumer of service status
     *   Consumer<Integer> statusConsumer = status -> {
     *       if (status == AmbientContextManager.STATUS_ACCESS_DENIED) {
     *         // User did not consent event detection. See #queryAmbientContextServiceStatus and
     *         // #startConsentActivity
     *       }
     *   };
     *
     *   // Register as observer
     *   AmbientContextManager ambientContextManager =
     *       context.getSystemService(AmbientContextManager.class);
     *   ambientContextManager.registerObserver(request, pendingIntent, executor, statusConsumer);
     *
     *   // Handle the list of {@link AmbientContextEvent}s in your receiver
     *   {@literal @}Override
     *   protected void onReceive(Context context, Intent intent) {
     *     List<AmbientContextEvent> events = AmbientContextManager.getEventsFromIntent(intent);
     *     if (!events.isEmpty()) {
     *       // Do something useful with the events.
     *     }
     *   }
     * </code></pre>
     *
     * @param request The request with events to observe.
     * @param pendingIntent A mutable {@link PendingIntent} that will be dispatched when any
     *                     requested event is detected.
     * @param resultPendingIntent A mutable {@link PendingIntent} that will be dispatched after the
     *                            requested events are detected.
     * @param executor Executor on which to run the consumer callback.
     * @param statusConsumer A consumer that handles the status code, which is returned
     *                      right after the call.
     */
    @RequiresPermission(Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT)
    public void registerObserver(
            @NonNull AmbientContextEventRequest request,
            @NonNull PendingIntent pendingIntent) {
        Preconditions.checkArgument(!pendingIntent.isImmutable());
            @NonNull PendingIntent resultPendingIntent,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull @StatusCode Consumer<Integer> statusConsumer) {
        Preconditions.checkArgument(!resultPendingIntent.isImmutable());
        try {
            RemoteCallback callback = new RemoteCallback(result -> {
                int statusCode =  result.getInt(STATUS_RESPONSE_BUNDLE_KEY);
                final long identity = Binder.clearCallingIdentity();
                try {
            mService.registerObserver(request, pendingIntent);
                    executor.execute(() -> statusConsumer.accept(statusCode));
                } finally {
                    Binder.restoreCallingIdentity(identity);
                }
            });
            mService.registerObserver(request, resultPendingIntent, callback);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
+9 −3

File changed and moved.

Preview size limit exceeded, changes collapsed.

Loading