blob: 159481f82e39bbe55fd64153b292fbb598c46ce8 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.app.ambientcontext;
import android.Manifest;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
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.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
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 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 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_EVENTS =
"android.app.ambientcontext.extra.AMBIENT_CONTEXT_EVENTS";
/**
* 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
})
@Retention(RetentionPolicy.SOURCE)
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 list of events, or an empty list if the intent doesn't have such events.
*/
@NonNull public static List<AmbientContextEvent> getEventsFromIntent(@NonNull Intent intent) {
if (intent.hasExtra(AmbientContextManager.EXTRA_AMBIENT_CONTEXT_EVENTS)) {
return intent.getParcelableArrayListExtra(EXTRA_AMBIENT_CONTEXT_EVENTS,
android.app.ambientcontext.AmbientContextEvent.class);
} else {
return new ArrayList<>();
}
}
private final Context mContext;
private final IAmbientContextManager mService;
/**
* {@hide}
*/
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.
* If the AmbientContextRequest contains a mixed set of events containing values both greater
* than and less than {@link AmbientContextEvent.EVENT_VENDOR_WEARABLE_START}, the request
* will be rejected with {@link AmbientContextManager#STATUS_NOT_SUPPORTED}.
* <p />
*
* Example:
*
* <pre><code>
* Set<Integer> eventTypes = new HashSet<>();
* eventTypes.add(AmbientContextEvent.EVENT_COUGH);
* eventTypes.add(AmbientContextEvent.EVENT_SNORE);
*
* // Create Consumer
* Consumer<Integer> statusConsumer = status -> {
* 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.
* If the eventTypes contains a mixed set of events containing values both greater than and less
* than {@link AmbientContextEvent.EVENT_VENDOR_WEARABLE_START}, the request will be rejected
* with {@link AmbientContextManager#STATUS_NOT_SUPPORTED}.
*
* @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.
* If the AmbientContextRequest contains a mixed set of events containing values both greater
* than and less than {@link AmbientContextEvent.EVENT_VENDOR_WEARABLE_START}, the request
* will be rejected with {@link AmbientContextManager#STATUS_NOT_SUPPORTED}.
* <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 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 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 {
executor.execute(() -> statusConsumer.accept(statusCode));
} finally {
Binder.restoreCallingIdentity(identity);
}
});
mService.registerObserver(request, resultPendingIntent, callback);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Allows app to register as a {@link AmbientContextEvent} observer. Same as {@link
* #registerObserver(AmbientContextEventRequest, PendingIntent, Executor, Consumer)},
* but use {@link AmbientContextCallback} instead of {@link PendingIntent} as a callback on
* detected events.
* Registering another observer from the same package that has already been
* registered will override the previous observer. If the same app previously calls
* {@link #registerObserver(AmbientContextEventRequest, AmbientContextCallback, Executor)},
* and now calls
* {@link #registerObserver(AmbientContextEventRequest, PendingIntent, Executor, Consumer)},
* the previous observer will be replaced with the new observer with the PendingIntent callback.
* Or vice versa.
* If the AmbientContextRequest contains a mixed set of events containing values both greater
* than and less than {@link AmbientContextEvent.EVENT_VENDOR_WEARABLE_START}, the request
* will be rejected with {@link AmbientContextManager#STATUS_NOT_SUPPORTED}.
*
* When the registration completes, a status will be returned to client through
* {@link AmbientContextCallback#onRegistrationComplete(int)}.
* If the AmbientContextManager service is not enabled yet, or the underlying detection service
* is not running yet, {@link AmbientContextManager#STATUS_SERVICE_UNAVAILABLE} will be
* returned, and the detection won't be really started.
* If the underlying detection service feature is not enabled, or the requested event type is
* not enabled yet, {@link AmbientContextManager#STATUS_NOT_SUPPORTED} will be returned, and the
* detection won't be really started.
* If there is no user consent, {@link AmbientContextManager#STATUS_ACCESS_DENIED} will be
* returned, and the detection won't be really started.
* Otherwise, it will try to start the detection. And if it starts successfully, it will return
* {@link AmbientContextManager#STATUS_SUCCESS}. If it fails to start the detection, then
* it will return {@link AmbientContextManager#STATUS_SERVICE_UNAVAILABLE}
* After registerObserver succeeds and when the service detects an event, the service will
* trigger {@link AmbientContextCallback#onEvents(List)}.
*
* @hide
*/
@RequiresPermission(Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT)
public void registerObserver(
@NonNull AmbientContextEventRequest request,
@NonNull @CallbackExecutor Executor executor,
@NonNull AmbientContextCallback ambientContextCallback) {
try {
IAmbientContextObserver observer = new IAmbientContextObserver.Stub() {
@Override
public void onEvents(List<AmbientContextEvent> events) throws RemoteException {
final long identity = Binder.clearCallingIdentity();
try {
executor.execute(() -> ambientContextCallback.onEvents(events));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
@Override
public void onRegistrationComplete(int statusCode) throws RemoteException {
final long identity = Binder.clearCallingIdentity();
try {
executor.execute(
() -> ambientContextCallback.onRegistrationComplete(statusCode));
} finally {
Binder.restoreCallingIdentity(identity);
}
}
};
mService.registerObserverWithCallback(request, mContext.getPackageName(), observer);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Unregisters the requesting app as an {@code AmbientContextEvent} observer. Unregistering an
* observer that was already unregistered or never registered will have no effect.
*/
@RequiresPermission(Manifest.permission.ACCESS_AMBIENT_CONTEXT_EVENT)
public void unregisterObserver() {
try {
mService.unregisterObserver(mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}