395 lines
17 KiB
Java
395 lines
17 KiB
Java
/*
|
|
* 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();
|
|
}
|
|
}
|
|
}
|