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