/* * 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.service.wearable; import android.annotation.BinderThread; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.app.Service; import android.app.ambientcontext.AmbientContextEvent; import android.app.ambientcontext.AmbientContextEventRequest; import android.app.wearable.Flags; import android.app.wearable.IWearableSensingCallback; import android.app.wearable.WearableSensingDataRequest; import android.app.wearable.WearableSensingManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; import android.os.Process; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.SharedMemory; import android.service.ambientcontext.AmbientContextDetectionResult; import android.service.ambientcontext.AmbientContextDetectionServiceStatus; import android.service.voice.HotwordAudioStream; import android.text.TextUtils; import android.util.Slog; import android.util.SparseArray; import com.android.internal.infra.AndroidFuture; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.time.Duration; import java.util.Arrays; import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.function.Consumer; /** * Abstract base class for sensing with wearable devices. An example of this is {@link *AmbientContextEvent} detection. * *

A service that provides requested sensing events to the system, such as a {@link *AmbientContextEvent}. The system's default WearableSensingService implementation is configured in * {@code config_defaultWearableSensingService}. If this config has no value, a stub is * returned. * *

An implementation of a WearableSensingService should be an isolated service. Using the * "isolatedProcess=true" attribute in the service's configurations.

** *
 * {@literal
 * 
 * }
 * 
* *

The use of "Wearable" here is not the same as the Android Wear platform and should be treated * separately.

* * @hide */ @SystemApi public abstract class WearableSensingService extends Service { private static final String TAG = WearableSensingService.class.getSimpleName(); /** * The bundle key for this class of object, used in {@code RemoteCallback#sendResult}. * * @hide */ public static final String STATUS_RESPONSE_BUNDLE_KEY = "android.app.wearable.WearableSensingStatusBundleKey"; /** * The bundle key for hotword audio stream, used in {@code RemoteCallback#sendResult}. * * @hide */ public static final String HOTWORD_AUDIO_STREAM_BUNDLE_KEY = "android.app.wearable.HotwordAudioStreamBundleKey"; /** * The {@link Intent} that must be declared as handled by the service. To be supported, the * service must also require the * {@link android.Manifest.permission#BIND_WEARABLE_SENSING_SERVICE} * permission so that other applications can not abuse it. */ public static final String SERVICE_INTERFACE = "android.service.wearable.WearableSensingService"; // Timeout to prevent thread from waiting on the openFile future indefinitely. private static final Duration OPEN_FILE_TIMEOUT = Duration.ofSeconds(5); private final SparseArray mDataRequestObserverIdToRequesterMap = new SparseArray<>(); private IWearableSensingCallback mWearableSensingCallback; @Nullable @Override public final IBinder onBind(@NonNull Intent intent) { if (SERVICE_INTERFACE.equals(intent.getAction())) { return new IWearableSensingService.Stub() { /** {@inheritDoc} */ @Override public void provideSecureConnection( ParcelFileDescriptor secureWearableConnection, IWearableSensingCallback wearableSensingCallback, RemoteCallback callback) { Objects.requireNonNull(secureWearableConnection); if (wearableSensingCallback != null) { mWearableSensingCallback = wearableSensingCallback; } Consumer consumer = createWearableStatusConsumer(callback); WearableSensingService.this.onSecureConnectionProvided( secureWearableConnection, consumer); } /** {@inheritDoc} */ @Override public void provideDataStream( ParcelFileDescriptor parcelFileDescriptor, IWearableSensingCallback wearableSensingCallback, RemoteCallback callback) { Objects.requireNonNull(parcelFileDescriptor); if (wearableSensingCallback != null) { mWearableSensingCallback = wearableSensingCallback; } Consumer consumer = createWearableStatusConsumer(callback); WearableSensingService.this.onDataStreamProvided( parcelFileDescriptor, consumer); } /** {@inheritDoc} */ @Override public void provideData( PersistableBundle data, SharedMemory sharedMemory, RemoteCallback callback) { Objects.requireNonNull(data); Consumer consumer = createWearableStatusConsumer(callback); WearableSensingService.this.onDataProvided(data, sharedMemory, consumer); } /** {@inheritDoc} */ @Override public void registerDataRequestObserver( int dataType, RemoteCallback dataRequestCallback, int dataRequestObserverId, String packageName, RemoteCallback statusCallback) { Objects.requireNonNull(dataRequestCallback); Objects.requireNonNull(statusCallback); WearableSensingDataRequester dataRequester; synchronized (mDataRequestObserverIdToRequesterMap) { dataRequester = mDataRequestObserverIdToRequesterMap.get(dataRequestObserverId); if (dataRequester == null) { dataRequester = createDataRequester(dataRequestCallback); mDataRequestObserverIdToRequesterMap.put( dataRequestObserverId, dataRequester); } } Consumer statusConsumer = createWearableStatusConsumer(statusCallback); WearableSensingService.this.onDataRequestObserverRegistered( dataType, packageName, dataRequester, statusConsumer); } @Override public void unregisterDataRequestObserver( int dataType, int dataRequestObserverId, String packageName, RemoteCallback statusCallback) { WearableSensingDataRequester dataRequester; synchronized (mDataRequestObserverIdToRequesterMap) { dataRequester = mDataRequestObserverIdToRequesterMap.get(dataRequestObserverId); if (dataRequester == null) { Slog.w( TAG, "dataRequestObserverId not found, cannot unregister data" + " request observer."); return; } mDataRequestObserverIdToRequesterMap.remove(dataRequestObserverId); } Consumer statusConsumer = createWearableStatusConsumer(statusCallback); WearableSensingService.this.onDataRequestObserverUnregistered( dataType, packageName, dataRequester, statusConsumer); } @Override public void startHotwordRecognition( RemoteCallback wearableHotwordCallback, RemoteCallback statusCallback) { Consumer hotwordAudioConsumer = (hotwordAudioStream) -> { Bundle bundle = new Bundle(); bundle.putParcelable( HOTWORD_AUDIO_STREAM_BUNDLE_KEY, hotwordAudioStream); wearableHotwordCallback.sendResult(bundle); }; Consumer statusConsumer = response -> { Bundle bundle = new Bundle(); bundle.putInt(STATUS_RESPONSE_BUNDLE_KEY, response); statusCallback.sendResult(bundle); }; WearableSensingService.this.onStartHotwordRecognition( hotwordAudioConsumer, statusConsumer); } /** {@inheritDoc} */ @Override public void stopHotwordRecognition(RemoteCallback statusCallback) { Consumer statusConsumer = response -> { Bundle bundle = new Bundle(); bundle.putInt(STATUS_RESPONSE_BUNDLE_KEY, response); statusCallback.sendResult(bundle); }; WearableSensingService.this.onStopHotwordRecognition(statusConsumer); } /** {@inheritDoc} */ @Override public void onValidatedByHotwordDetectionService() { WearableSensingService.this.onValidatedByHotwordDetectionService(); } /** {@inheritDoc} */ @Override public void stopActiveHotwordAudio() { WearableSensingService.this.onStopHotwordAudioStream(); } /** {@inheritDoc} */ @Override public void startDetection( @NonNull AmbientContextEventRequest request, String packageName, RemoteCallback detectionResultCallback, RemoteCallback statusCallback) { Objects.requireNonNull(request); Objects.requireNonNull(packageName); Objects.requireNonNull(detectionResultCallback); Objects.requireNonNull(statusCallback); Consumer detectionResultConsumer = result -> { Bundle bundle = new Bundle(); bundle.putParcelable( AmbientContextDetectionResult.RESULT_RESPONSE_BUNDLE_KEY, result); detectionResultCallback.sendResult(bundle); }; Consumer statusConsumer = status -> { Bundle bundle = new Bundle(); bundle.putParcelable( AmbientContextDetectionServiceStatus .STATUS_RESPONSE_BUNDLE_KEY, status); statusCallback.sendResult(bundle); }; WearableSensingService.this.onStartDetection( request, packageName, statusConsumer, detectionResultConsumer); Slog.d(TAG, "startDetection " + request); } /** {@inheritDoc} */ @Override public void stopDetection(String packageName) { Objects.requireNonNull(packageName); WearableSensingService.this.onStopDetection(packageName); } /** {@inheritDoc} */ @Override public void queryServiceStatus( @AmbientContextEvent.EventCode int[] eventTypes, String packageName, RemoteCallback callback) { Objects.requireNonNull(eventTypes); Objects.requireNonNull(packageName); Objects.requireNonNull(callback); Consumer consumer = response -> { Bundle bundle = new Bundle(); bundle.putParcelable( AmbientContextDetectionServiceStatus .STATUS_RESPONSE_BUNDLE_KEY, response); callback.sendResult(bundle); }; Integer[] events = intArrayToIntegerArray(eventTypes); WearableSensingService.this.onQueryServiceStatus( new HashSet<>(Arrays.asList(events)), packageName, consumer); } /** {@inheritDoc} */ @Override public void killProcess() { Slog.d(TAG, "#killProcess"); Process.killProcess(Process.myPid()); } }; } Slog.w(TAG, "Incorrect service interface, returning null."); return null; } /** * Called when a secure connection to the wearable is available. See {@link * WearableSensingManager#provideConnection(ParcelFileDescriptor, Executor, Consumer)} * for details about the secure connection. * *

When the {@code secureWearableConnection} is closed, the system will send a {@link * WearableSensingManager#STATUS_CHANNEL_ERROR} status code to the status consumer provided by * the caller of {@link WearableSensingManager#provideConnection(ParcelFileDescriptor, * Executor, Consumer)}. * *

The implementing class should override this method. It should return an appropriate status * code via {@code statusConsumer} after receiving the {@code secureWearableConnection}. * * @param secureWearableConnection The secure connection to the wearable. * @param statusConsumer The consumer for the service status. */ @FlaggedApi(Flags.FLAG_ENABLE_PROVIDE_WEARABLE_CONNECTION_API) @BinderThread public void onSecureConnectionProvided( @NonNull ParcelFileDescriptor secureWearableConnection, @NonNull Consumer statusConsumer) { statusConsumer.accept(WearableSensingManager.STATUS_UNSUPPORTED_OPERATION); } /** * Called when a data stream to the wearable is provided. This data stream can be used to obtain * data from a wearable device. It is up to the implementation to maintain the data stream and * close the data stream when it is finished. * * @param parcelFileDescriptor The data stream to the wearable * @param statusConsumer the consumer for the service status. */ @BinderThread public abstract void onDataStreamProvided(@NonNull ParcelFileDescriptor parcelFileDescriptor, @NonNull Consumer statusConsumer); /** * Called when configurations and read-only data in a {@link PersistableBundle} can be used by * the WearableSensingService and sends the result to the {@link Consumer} right after the call. * It is dependent on the application to define the type of data to provide. This is used by * applications that will also provide an implementation of an isolated WearableSensingService. * If the data was provided successfully {@link WearableSensingManager#STATUS_SUCCESS} will be * provided. * * @param data Application configuration data to provide to the {@link WearableSensingService}. * PersistableBundle does not allow any remotable objects or other contents that can be used * to communicate with other processes. * @param sharedMemory The unrestricted data blob to provide to the {@link * WearableSensingService}. Use this to provide the sensing models data or other such data * to the trusted process. * @param statusConsumer the consumer for the service status. */ @BinderThread public abstract void onDataProvided( @NonNull PersistableBundle data, @Nullable SharedMemory sharedMemory, @NonNull Consumer statusConsumer); /** * Called when a data request observer is registered. Each request must not be larger than * {@link WearableSensingDataRequest#getMaxRequestSize()}. In addition, at most {@link * WearableSensingDataRequester#getRateLimit()} requests can be sent every rolling {@link * WearableSensingDataRequester#getRateLimitWindowSize()}. Requests that are too large or too * frequent will be dropped by the system. See {@link * WearableSensingDataRequester#requestData(WearableSensingDataRequest, Consumer)} for details * about the status code returned for each request. * *

The implementing class should override this method. After the data requester is received, * it should send a {@link WearableSensingManager#STATUS_SUCCESS} status code to the {@code * statusConsumer} unless it encounters an error condition described by a status code listed in * {@link WearableSensingManager}, such as {@link * WearableSensingManager#STATUS_WEARABLE_UNAVAILABLE}, in which case it should return the * corresponding status code. * * @param dataType The data type the observer is registered for. Values are defined by the * application that implements this class. * @param packageName The package name of the app that will receive the requests. * @param dataRequester A handle to the observer registered. It can be used to request data of * the specified data type. * @param statusConsumer the consumer for the status of the data request observer registration. * This is different from the status for each data request. */ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API) @BinderThread public void onDataRequestObserverRegistered( int dataType, @NonNull String packageName, @NonNull WearableSensingDataRequester dataRequester, @NonNull Consumer statusConsumer) { statusConsumer.accept(WearableSensingManager.STATUS_UNSUPPORTED_OPERATION); } /** * Called when a data request observer is unregistered. * *

The implementing class should override this method. It should send a {@link * WearableSensingManager#STATUS_SUCCESS} status code to the {@code statusConsumer} unless it * encounters an error condition described by a status code listed in {@link * WearableSensingManager}, such as {@link WearableSensingManager#STATUS_WEARABLE_UNAVAILABLE}, * in which case it should return the corresponding status code. * * @param dataType The data type the observer is for. * @param packageName The package name of the app that will receive the requests sent to the * dataRequester. * @param dataRequester A handle to the observer to be unregistered. It is the exact same * instance provided in a previous {@link #onDataRequestConsumerRegistered(int, String, * WearableSensingDataRequester, Consumer)} invocation. * @param statusConsumer the consumer for the status of the data request observer * unregistration. This is different from the status for each data request. */ @FlaggedApi(Flags.FLAG_ENABLE_DATA_REQUEST_OBSERVER_API) @BinderThread public void onDataRequestObserverUnregistered( int dataType, @NonNull String packageName, @NonNull WearableSensingDataRequester dataRequester, @NonNull Consumer statusConsumer) { statusConsumer.accept(WearableSensingManager.STATUS_UNSUPPORTED_OPERATION); } /** * Called when the wearable is requested to start hotword recognition. * *

This method is expected to be overridden by a derived class. The implementation should * store the {@code hotwordAudioConsumer} and send it the audio data when first-stage hotword is * detected from the wearable. It should also send a {@link * WearableSensingManager#STATUS_SUCCESS} status code to the {@code statusConsumer} unless it * encounters an error condition described by a status code listed in {@link * WearableSensingManager}, such as {@link WearableSensingManager#STATUS_WEARABLE_UNAVAILABLE}, * in which case it should return the corresponding status code. * *

The implementation should also store the {@code statusConsumer}. If the wearable stops * listening for hotword for any reason other than {@link #onStopListeningForHotword(Consumer)} * being invoked, it should send an appropriate status code listed in {@link * WearableSensingManager} to {@code statusConsumer}. If the error condition cannot be described * by any of those status codes, it should send a {@link WearableSensingManager#STATUS_UNKNOWN}. * *

If this method is called again, the implementation should use the new {@code * hotwordAudioConsumer} and discard any previous ones it received. * *

At this time, the {@code timestamp} field in the {@link HotwordAudioStream} is not used * and will be discarded by the system. * * @param hotwordAudioConsumer The consumer for the wearable hotword audio data. * @param statusConsumer The consumer for the service status. */ @FlaggedApi(Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API) @BinderThread public void onStartHotwordRecognition( @NonNull Consumer hotwordAudioConsumer, @NonNull Consumer statusConsumer) { if (Flags.enableUnsupportedOperationStatusCode()) { statusConsumer.accept(WearableSensingManager.STATUS_UNSUPPORTED_OPERATION); } } /** * Called when the wearable is requested to stop hotword recognition. * *

This method is expected to be overridden by a derived class. It should send a {@link * WearableSensingManager#STATUS_SUCCESS} status code to the {@code statusConsumer} unless it * encounters an error condition described by a status code listed in {@link * WearableSensingManager}, such as {@link WearableSensingManager#STATUS_WEARABLE_UNAVAILABLE}, * in which case it should return the corresponding status code. * * @param statusConsumer The consumer for the service status. */ @FlaggedApi(Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API) @BinderThread public void onStopHotwordRecognition(@NonNull Consumer statusConsumer) { if (Flags.enableUnsupportedOperationStatusCode()) { statusConsumer.accept(WearableSensingManager.STATUS_UNSUPPORTED_OPERATION); } } /** * Called when hotword audio data sent to the {@code hotwordAudioConsumer} in {@link * #onStartListeningForHotword(Consumer, Consumer)} is accepted by the * {@link android.service.voice.HotwordDetectionService} as valid hotword. * *

After the implementation of this class sends the hotword audio data to the {@code * hotwordAudioConsumer} in {@link #onStartListeningForHotword(Consumer, * Consumer)}, the system will forward the data into {@link * android.service.voice.HotwordDetectionService} (which runs in an isolated process) for * second-stage hotword detection. If accepted as valid hotword there, this method will be * called, and then the system will send the data to the currently active {@link * android.service.voice.AlwaysOnHotwordDetector} (which may not run in an isolated process). * *

This method is expected to be overridden by a derived class. The implementation must * request the wearable to turn on the microphone indicator to notify the user that audio data * is being used outside of the isolated environment. */ @FlaggedApi(Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API) @BinderThread public void onValidatedByHotwordDetectionService() {} /** * Called when the currently active hotword audio stream is no longer needed. * *

This method can be called as a result of hotword rejection by {@link * android.service.voice.HotwordDetectionService}, or the {@link * android.service.voice.AlwaysOnHotwordDetector} closing the data stream it received, or a * non-recoverable error occurred before the data reaches the {@link * android.service.voice.HotwordDetectionService} or the {@link * android.service.voice.AlwaysOnHotwordDetector}. * *

This method is expected to be overridden by a derived class. The implementation should * stop sending hotword audio data to the {@code hotwordAudioConsumer} in {@link * #onStartListeningForHotword(Consumer, Consumer)} */ @FlaggedApi(Flags.FLAG_ENABLE_HOTWORD_WEARABLE_SENSING_API) @BinderThread public void onStopHotwordAudioStream() {} /** * Called when a client app requests starting detection of the events in the request. The * implementation should keep track of whether the user has explicitly consented to detecting * the events using on-going ambient sensor (e.g. microphone), and agreed to share the * detection results with this client app. If the user has not consented, the detection * should not start, and the statusConsumer should get a response with STATUS_ACCESS_DENIED. * If the user has made the consent and the underlying services are available, the * implementation should start detection and provide detected events to the * detectionResultConsumer. If the type of event needs immediate attention, the implementation * should send result as soon as detected. Otherwise, the implementation can batch response. * The ongoing detection will keep running, until onStopDetection is called. If there were * previously requested detections from the same package, regardless of the type of events in * the request, the previous request will be replaced with the new request and pending events * are discarded. * * @param request The request with events to detect. * @param packageName the requesting app's package name * @param statusConsumer the consumer for the service status. * @param detectionResultConsumer the consumer for the detected event */ @BinderThread public abstract void onStartDetection(@NonNull AmbientContextEventRequest request, @NonNull String packageName, @NonNull Consumer statusConsumer, @NonNull Consumer detectionResultConsumer); /** * Stops detection of the events. Events that are not being detected will be ignored. * * @param packageName stops detection for the given package. */ public abstract void onStopDetection(@NonNull String packageName); /** * Called when a query for the detection status occurs. The implementation should check * the detection status of the requested events for the package, and provide results in a * {@link AmbientContextDetectionServiceStatus} for the consumer. * * @param eventTypes The events to check for status. * @param packageName the requesting app's package name * @param consumer the consumer for the query results */ @BinderThread public abstract void onQueryServiceStatus(@NonNull Set eventTypes, @NonNull String packageName, @NonNull Consumer consumer); /** * Overrides {@link Context#openFileInput} to read files with the given {@code fileName} under * the internal app storage of the APK providing the implementation for this class. {@link * Context#getFilesDir()} will be added as a prefix to the provided {@code fileName}. * *

This method is only functional after {@link * #onSecureConnectionProvided(ParcelFileDescriptor, Consumer)} or {@link * #onDataStreamProvided(ParcelFileDescriptor, Consumer)} has been called as a result of a * process owned by the same APK calling {@link * WearableSensingManager#provideConnection(ParcelFileDescriptor, Executor, Consumer)} or {@link * WearableSensingManager#provideDataStream(ParcelFileDescriptor, Executor, Consumer)}. * Otherwise, it will throw an {@link IllegalStateException}. This is because this method * proxies the file read via that process. Also, the APK needs to have a targetSdkVersion of 35 * or newer. * * @param fileName Relative path of a file under {@link Context#getFilesDir()}. * @throws IllegalStateException if the above condition is not satisfied. * @throws FileNotFoundException if the file does not exist or cannot be opened, or an error * occurred during the RPC to proxy the file read via a non-isolated process. */ // SuppressLint is needed because the parent Context class does not specify the nullability of // the parameter filename. If we remove the @NonNull annotation, the linter will complain about // MissingNullability @Override public @NonNull FileInputStream openFileInput( @SuppressLint("InvalidNullabilityOverride") @NonNull String fileName) throws FileNotFoundException { if (fileName == null) { throw new IllegalArgumentException("filename cannot be null"); } try { if (mWearableSensingCallback == null) { throw new IllegalStateException( "Cannot open file from WearableSensingService. WearableSensingCallback is" + " not available."); } AndroidFuture future = new AndroidFuture<>(); mWearableSensingCallback.openFile(fileName, future); ParcelFileDescriptor pfd = future.get(OPEN_FILE_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS); if (pfd == null) { throw new FileNotFoundException( TextUtils.formatSimple( "File %s not found or unable to be opened in read-only mode.", fileName)); } return new FileInputStream(pfd.getFileDescriptor()); } catch (RemoteException | ExecutionException | TimeoutException e) { throw (FileNotFoundException) new FileNotFoundException("Cannot open file due to remote service failure") .initCause(e); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw (FileNotFoundException) new FileNotFoundException("Interrupted when opening a file.").initCause(e); } } @NonNull private static Integer[] intArrayToIntegerArray(@NonNull int[] integerSet) { Integer[] intArray = new Integer[integerSet.length]; int i = 0; for (Integer type : integerSet) { intArray[i++] = type; } return intArray; } private static WearableSensingDataRequester createDataRequester( RemoteCallback dataRequestCallback) { return (request, requestStatusConsumer) -> { Bundle bundle = new Bundle(); bundle.putParcelable(WearableSensingDataRequest.REQUEST_BUNDLE_KEY, request); RemoteCallback requestStatusCallback = new RemoteCallback( requestStatusBundle -> { requestStatusConsumer.accept( requestStatusBundle.getInt( WearableSensingManager.STATUS_RESPONSE_BUNDLE_KEY)); }); bundle.putParcelable( WearableSensingDataRequest.REQUEST_STATUS_CALLBACK_BUNDLE_KEY, requestStatusCallback); dataRequestCallback.sendResult(bundle); }; } @NonNull private static Consumer createWearableStatusConsumer(RemoteCallback statusCallback) { return response -> { Bundle bundle = new Bundle(); bundle.putInt(STATUS_RESPONSE_BUNDLE_KEY, response); statusCallback.sendResult(bundle); }; } }