1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.service.voice; 18 19 import android.annotation.CallSuper; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.app.ActivityThread; 23 import android.media.AudioFormat; 24 import android.media.permission.Identity; 25 import android.os.Binder; 26 import android.os.Handler; 27 import android.os.HandlerExecutor; 28 import android.os.IBinder; 29 import android.os.Looper; 30 import android.os.ParcelFileDescriptor; 31 import android.os.PersistableBundle; 32 import android.os.RemoteException; 33 import android.os.SharedMemory; 34 import android.util.Slog; 35 36 import com.android.internal.app.IHotwordRecognitionStatusCallback; 37 import com.android.internal.app.IVoiceInteractionManagerService; 38 39 import java.util.concurrent.Executor; 40 import java.util.concurrent.atomic.AtomicBoolean; 41 import java.util.function.Consumer; 42 43 /** Base implementation of {@link HotwordDetector}. 44 * 45 * This class provides methods to manage the detector lifecycle for both 46 * {@link HotwordDetectionService} and {@link VisualQueryDetectionService}. We keep the name of the 47 * interface {@link HotwordDetector} since {@link VisualQueryDetectionService} can be logically 48 * treated as a visual activation hotword detection and also because of the existing public 49 * interface. To avoid confusion on the naming between the trusted hotword framework and the actual 50 * isolated {@link HotwordDetectionService}, the hotword from the names is removed. 51 */ 52 abstract class AbstractDetector implements HotwordDetector { 53 private static final String TAG = AbstractDetector.class.getSimpleName(); 54 private static final boolean DEBUG = false; 55 56 protected final Object mLock = new Object(); 57 58 private final IVoiceInteractionManagerService mManagerService; 59 private final Executor mExecutor; 60 private final HotwordDetector.Callback mCallback; 61 private Consumer<AbstractDetector> mOnDestroyListener; 62 private final AtomicBoolean mIsDetectorActive; 63 /** 64 * A token which is used by voice interaction system service to identify different detectors. 65 */ 66 private final IBinder mToken = new Binder(); 67 AbstractDetector( IVoiceInteractionManagerService managerService, Executor executor, HotwordDetector.Callback callback)68 AbstractDetector( 69 IVoiceInteractionManagerService managerService, 70 Executor executor, 71 HotwordDetector.Callback callback) { 72 mManagerService = managerService; 73 mCallback = callback; 74 mExecutor = executor != null ? executor : new HandlerExecutor( 75 new Handler(Looper.getMainLooper())); 76 mIsDetectorActive = new AtomicBoolean(true); 77 } 78 isSameToken(IBinder token)79 boolean isSameToken(IBinder token) { 80 if (token == null) { 81 return false; 82 } 83 return mToken == token; 84 } 85 86 /** 87 * Method to be called for the detector to ready/register itself with underlying system 88 * services. 89 */ initialize(@ullable PersistableBundle options, @Nullable SharedMemory sharedMemory)90 abstract void initialize(@Nullable PersistableBundle options, 91 @Nullable SharedMemory sharedMemory); 92 93 /** 94 * Detect from an externally supplied stream of data. 95 * 96 * @return {@code true} if the request to start recognition succeeded 97 */ 98 @Override startRecognition( @onNull ParcelFileDescriptor audioStream, @NonNull AudioFormat audioFormat, @Nullable PersistableBundle options)99 public boolean startRecognition( 100 @NonNull ParcelFileDescriptor audioStream, 101 @NonNull AudioFormat audioFormat, 102 @Nullable PersistableBundle options) { 103 if (DEBUG) { 104 Slog.i(TAG, "#recognizeHotword"); 105 } 106 throwIfDetectorIsNoLongerActive(); 107 108 // TODO: consider closing existing session. 109 110 try { 111 mManagerService.startListeningFromExternalSource( 112 audioStream, 113 audioFormat, 114 options, 115 mToken, 116 new BinderCallback(mExecutor, mCallback)); 117 } catch (RemoteException e) { 118 e.rethrowFromSystemServer(); 119 } 120 121 return true; 122 } 123 124 /** 125 * Set configuration and pass read-only data to trusted detection service. 126 * 127 * @param options Application configuration data to provide to the 128 * {@link VisualQueryDetectionService} and {@link HotwordDetectionService}. 129 * PersistableBundle does not allow any remotable objects or other contents that can be 130 * used to communicate with other processes. 131 * @param sharedMemory The unrestricted data blob to provide to the 132 * {@link VisualQueryDetectionService} and {@link HotwordDetectionService}. Use this to 133 * provide the hotword models data or other such data to the trusted process. 134 * @throws IllegalStateException if this {@link HotwordDetector} wasn't specified to use a 135 * {@link HotwordDetectionService} or {@link VisualQueryDetectionService} when it was 136 * created. 137 */ 138 @Override updateState(@ullable PersistableBundle options, @Nullable SharedMemory sharedMemory)139 public void updateState(@Nullable PersistableBundle options, 140 @Nullable SharedMemory sharedMemory) { 141 if (DEBUG) { 142 Slog.d(TAG, "updateState()"); 143 } 144 throwIfDetectorIsNoLongerActive(); 145 try { 146 mManagerService.updateState(options, sharedMemory, mToken); 147 } catch (RemoteException e) { 148 throw e.rethrowFromSystemServer(); 149 } 150 } 151 initAndVerifyDetector( @ullable PersistableBundle options, @Nullable SharedMemory sharedMemory, @NonNull IHotwordRecognitionStatusCallback callback, int detectorType)152 protected void initAndVerifyDetector( 153 @Nullable PersistableBundle options, 154 @Nullable SharedMemory sharedMemory, 155 @NonNull IHotwordRecognitionStatusCallback callback, 156 int detectorType) { 157 if (DEBUG) { 158 Slog.d(TAG, "initAndVerifyDetector()"); 159 } 160 Identity identity = new Identity(); 161 identity.packageName = ActivityThread.currentOpPackageName(); 162 try { 163 mManagerService.initAndVerifyDetector(identity, options, sharedMemory, mToken, callback, 164 detectorType); 165 } catch (RemoteException e) { 166 throw e.rethrowFromSystemServer(); 167 } 168 } 169 registerOnDestroyListener(Consumer<AbstractDetector> onDestroyListener)170 void registerOnDestroyListener(Consumer<AbstractDetector> onDestroyListener) { 171 synchronized (mLock) { 172 if (mOnDestroyListener != null) { 173 throw new IllegalStateException("only one destroy listener can be registered"); 174 } 175 mOnDestroyListener = onDestroyListener; 176 } 177 } 178 179 @CallSuper 180 @Override destroy()181 public void destroy() { 182 if (!mIsDetectorActive.get()) { 183 return; 184 } 185 mIsDetectorActive.set(false); 186 try { 187 mManagerService.destroyDetector(mToken); 188 } catch (RemoteException e) { 189 throw e.rethrowFromSystemServer(); 190 } 191 synchronized (mLock) { 192 mOnDestroyListener.accept(this); 193 } 194 } 195 throwIfDetectorIsNoLongerActive()196 protected void throwIfDetectorIsNoLongerActive() { 197 if (!mIsDetectorActive.get()) { 198 Slog.e(TAG, "attempting to use a destroyed detector which is no longer active"); 199 throw new IllegalStateException( 200 "attempting to use a destroyed detector which is no longer active"); 201 } 202 } 203 204 private static class BinderCallback 205 extends IMicrophoneHotwordDetectionVoiceInteractionCallback.Stub { 206 // TODO: these need to be weak references. 207 private final HotwordDetector.Callback mCallback; 208 private final Executor mExecutor; 209 BinderCallback(Executor executor, HotwordDetector.Callback callback)210 BinderCallback(Executor executor, HotwordDetector.Callback callback) { 211 this.mCallback = callback; 212 this.mExecutor = executor; 213 } 214 215 /** TODO: onDetected */ 216 @Override onDetected( @ullable HotwordDetectedResult hotwordDetectedResult, @Nullable AudioFormat audioFormat, @Nullable ParcelFileDescriptor audioStreamIgnored)217 public void onDetected( 218 @Nullable HotwordDetectedResult hotwordDetectedResult, 219 @Nullable AudioFormat audioFormat, 220 @Nullable ParcelFileDescriptor audioStreamIgnored) { 221 Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> { 222 mCallback.onDetected(new AlwaysOnHotwordDetector.EventPayload.Builder() 223 .setCaptureAudioFormat(audioFormat) 224 .setHotwordDetectedResult(hotwordDetectedResult) 225 .build()); 226 })); 227 } 228 229 /** Called when the detection fails due to an error. */ 230 @Override onHotwordDetectionServiceFailure( HotwordDetectionServiceFailure hotwordDetectionServiceFailure)231 public void onHotwordDetectionServiceFailure( 232 HotwordDetectionServiceFailure hotwordDetectionServiceFailure) { 233 Slog.v(TAG, "BinderCallback#onHotwordDetectionServiceFailure: " 234 + hotwordDetectionServiceFailure); 235 Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> { 236 if (hotwordDetectionServiceFailure != null) { 237 mCallback.onFailure(hotwordDetectionServiceFailure); 238 } else { 239 mCallback.onUnknownFailure("Error data is null"); 240 } 241 })); 242 } 243 244 @Override onRejected(@ullable HotwordRejectedResult result)245 public void onRejected(@Nullable HotwordRejectedResult result) { 246 Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> { 247 mCallback.onRejected( 248 result != null ? result : new HotwordRejectedResult.Builder().build()); 249 })); 250 } 251 } 252 } 253