1 /*
2  * Copyright (C) 2018 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.media.soundtrigger;
18 
19 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
20 
21 import android.annotation.CallSuper;
22 import android.annotation.MainThread;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.annotation.SystemApi;
26 import android.app.Service;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.hardware.soundtrigger.SoundTrigger;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.IBinder;
33 import android.os.ParcelUuid;
34 import android.os.RemoteException;
35 import android.util.ArrayMap;
36 import android.util.Log;
37 
38 import com.android.internal.annotations.GuardedBy;
39 
40 import java.util.UUID;
41 
42 /**
43  * A service that allows interaction with the actual sound trigger detection on the system.
44  *
45  * <p> Sound trigger detection refers to detectors that match generic sound patterns that are
46  * not voice-based. The voice-based recognition models should utilize the {@link
47  * android.service.voice.VoiceInteractionService} instead. Access to this class needs to be
48  * protected by the {@value android.Manifest.permission.BIND_SOUND_TRIGGER_DETECTION_SERVICE}
49  * permission granted only to the system.
50  *
51  * <p>This service has to be explicitly started by an app, the system does not scan for and start
52  * these services.
53  *
54  * <p>If an operation ({@link #onGenericRecognitionEvent}, {@link #onError},
55  * {@link #onRecognitionPaused}, {@link #onRecognitionResumed}) is triggered the service is
56  * considered as running in the foreground. Once the operation is processed the service should call
57  * {@link #operationFinished(UUID, int)}. If this does not happen in
58  * {@link SoundTriggerManager#getDetectionServiceOperationsTimeout()} milliseconds
59  * {@link #onStopOperation(UUID, Bundle, int)} is called and the service is unbound.
60  *
61  * <p>The total amount of operations per day might be limited.
62  *
63  * @hide
64  */
65 @SystemApi
66 public abstract class SoundTriggerDetectionService extends Service {
67     private static final String LOG_TAG = SoundTriggerDetectionService.class.getSimpleName();
68 
69     private static final boolean DEBUG = false;
70 
71     private final Object mLock = new Object();
72 
73     /**
74      * Client indexed by model uuid. This is needed for the {@link #operationFinished(UUID, int)}
75      * callbacks.
76      */
77     @GuardedBy("mLock")
78     private final ArrayMap<UUID, ISoundTriggerDetectionServiceClient> mClients =
79             new ArrayMap<>();
80 
81     private Handler mHandler;
82 
83     /**
84      * @hide
85      */
86     @Override
attachBaseContext(Context base)87     protected final void attachBaseContext(Context base) {
88         super.attachBaseContext(base);
89         mHandler = new Handler(base.getMainLooper());
90     }
91 
setClient(@onNull UUID uuid, @Nullable Bundle params, @NonNull ISoundTriggerDetectionServiceClient client)92     private void setClient(@NonNull UUID uuid, @Nullable Bundle params,
93             @NonNull ISoundTriggerDetectionServiceClient client) {
94         if (DEBUG) Log.i(LOG_TAG, uuid + ": handle setClient");
95 
96         synchronized (mLock) {
97             mClients.put(uuid, client);
98         }
99         onConnected(uuid, params);
100     }
101 
removeClient(@onNull UUID uuid, @Nullable Bundle params)102     private void removeClient(@NonNull UUID uuid, @Nullable Bundle params) {
103         if (DEBUG) Log.i(LOG_TAG, uuid + ": handle removeClient");
104 
105         synchronized (mLock) {
106             mClients.remove(uuid);
107         }
108         onDisconnected(uuid, params);
109     }
110 
111     /**
112      * The system has connected to this service for the recognition registered for the model
113      * {@code uuid}.
114      *
115      * <p> This is called before any operations are delivered.
116      *
117      * @param uuid   The {@code uuid} of the model the recognitions is registered for
118      * @param params The {@code params} passed when the recognition was started
119      */
120     @MainThread
onConnected(@onNull UUID uuid, @Nullable Bundle params)121     public void onConnected(@NonNull UUID uuid, @Nullable Bundle params) {
122         /* do nothing */
123     }
124 
125     /**
126      * The system has disconnected from this service for the recognition registered for the model
127      * {@code uuid}.
128      *
129      * <p>Once this is called {@link #operationFinished} cannot be called anymore for
130      * {@code uuid}.
131      *
132      * <p> {@link #onConnected(UUID, Bundle)} is called before any further operations are delivered.
133      *
134      * @param uuid   The {@code uuid} of the model the recognitions is registered for
135      * @param params The {@code params} passed when the recognition was started
136      */
137     @MainThread
onDisconnected(@onNull UUID uuid, @Nullable Bundle params)138     public void onDisconnected(@NonNull UUID uuid, @Nullable Bundle params) {
139         /* do nothing */
140     }
141 
142     /**
143      * A new generic sound trigger event has been detected.
144      *
145      * @param uuid   The {@code uuid} of the model the recognition is registered for
146      * @param params The {@code params} passed when the recognition was started
147      * @param opId The id of this operation. Once the operation is done, this service needs to call
148      *             {@link #operationFinished(UUID, int)}
149      * @param event The event that has been detected
150      */
151     @MainThread
onGenericRecognitionEvent(@onNull UUID uuid, @Nullable Bundle params, int opId, @NonNull SoundTrigger.RecognitionEvent event)152     public void onGenericRecognitionEvent(@NonNull UUID uuid, @Nullable Bundle params, int opId,
153             @NonNull SoundTrigger.RecognitionEvent event) {
154         operationFinished(uuid, opId);
155     }
156 
157     /**
158      * A error has been detected.
159      *
160      * @param uuid   The {@code uuid} of the model the recognition is registered for
161      * @param params The {@code params} passed when the recognition was started
162      * @param opId The id of this operation. Once the operation is done, this service needs to call
163      *             {@link #operationFinished(UUID, int)}
164      * @param status The error code detected
165      */
166     @MainThread
onError(@onNull UUID uuid, @Nullable Bundle params, int opId, int status)167     public void onError(@NonNull UUID uuid, @Nullable Bundle params, int opId, int status) {
168         operationFinished(uuid, opId);
169     }
170 
171     /**
172      * An operation took too long and should be stopped.
173      *
174      * @param uuid   The {@code uuid} of the model the recognition is registered for
175      * @param params The {@code params} passed when the recognition was started
176      * @param opId The id of the operation that took too long
177      */
178     @MainThread
onStopOperation(@onNull UUID uuid, @Nullable Bundle params, int opId)179     public abstract void onStopOperation(@NonNull UUID uuid, @Nullable Bundle params, int opId);
180 
181     /**
182      * Tell that the system that an operation has been fully processed.
183      *
184      * @param uuid The {@code uuid} of the model the recognition is registered for
185      * @param opId The id of the operation that is processed
186      */
operationFinished(@ullable UUID uuid, int opId)187     public final void operationFinished(@Nullable UUID uuid, int opId) {
188         try {
189             ISoundTriggerDetectionServiceClient client;
190             synchronized (mLock) {
191                 client = mClients.get(uuid);
192 
193                 if (client == null) {
194                     Log.w(LOG_TAG, "operationFinished called, but no client for "
195                             + uuid + ". Was this called after onDisconnected?");
196                     return;
197                 }
198             }
199             client.onOpFinished(opId);
200         } catch (RemoteException e) {
201             Log.e(LOG_TAG, "operationFinished, remote exception for client " + uuid, e);
202         }
203     }
204 
205     /**
206      * @hide
207      */
208     @Override
onBind(Intent intent)209     public final IBinder onBind(Intent intent) {
210         return new ISoundTriggerDetectionService.Stub() {
211             private final Object mBinderLock = new Object();
212 
213             /** Cached params bundles indexed by the model uuid */
214             @GuardedBy("mBinderLock")
215             public final ArrayMap<UUID, Bundle> mParams = new ArrayMap<>();
216 
217             @Override
218             public void setClient(ParcelUuid puuid, Bundle params,
219                     ISoundTriggerDetectionServiceClient client) {
220                 UUID uuid = puuid.getUuid();
221                 synchronized (mBinderLock) {
222                     mParams.put(uuid, params);
223                 }
224 
225                 if (DEBUG) Log.i(LOG_TAG, uuid + ": setClient(" + params + ")");
226                 mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::setClient,
227                         SoundTriggerDetectionService.this, uuid, params, client));
228             }
229 
230             @Override
231             public void removeClient(ParcelUuid puuid) {
232                 UUID uuid = puuid.getUuid();
233                 Bundle params;
234                 synchronized (mBinderLock) {
235                     params = mParams.remove(uuid);
236                 }
237 
238                 if (DEBUG) Log.i(LOG_TAG, uuid + ": removeClient");
239                 mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::removeClient,
240                         SoundTriggerDetectionService.this, uuid, params));
241             }
242 
243             @Override
244             public void onGenericRecognitionEvent(ParcelUuid puuid, int opId,
245                     SoundTrigger.GenericRecognitionEvent event) {
246                 UUID uuid = puuid.getUuid();
247                 Bundle params;
248                 synchronized (mBinderLock) {
249                     params = mParams.get(uuid);
250                 }
251 
252                 if (DEBUG) Log.i(LOG_TAG, uuid + "(" + opId + "): onGenericRecognitionEvent");
253                 mHandler.sendMessage(
254                         obtainMessage(SoundTriggerDetectionService::onGenericRecognitionEvent,
255                                 SoundTriggerDetectionService.this, uuid, params, opId, event));
256             }
257 
258             @Override
259             public void onError(ParcelUuid puuid, int opId, int status) {
260                 UUID uuid = puuid.getUuid();
261                 Bundle params;
262                 synchronized (mBinderLock) {
263                     params = mParams.get(uuid);
264                 }
265 
266                 if (DEBUG) Log.i(LOG_TAG, uuid + "(" + opId + "): onError(" + status + ")");
267                 mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::onError,
268                         SoundTriggerDetectionService.this, uuid, params, opId, status));
269             }
270 
271             @Override
272             public void onStopOperation(ParcelUuid puuid, int opId) {
273                 UUID uuid = puuid.getUuid();
274                 Bundle params;
275                 synchronized (mBinderLock) {
276                     params = mParams.get(uuid);
277                 }
278 
279                 if (DEBUG) Log.i(LOG_TAG, uuid + "(" + opId + "): onStopOperation");
280                 mHandler.sendMessage(obtainMessage(SoundTriggerDetectionService::onStopOperation,
281                         SoundTriggerDetectionService.this, uuid, params, opId));
282             }
283         };
284     }
285 
286     @CallSuper
287     @Override
onUnbind(Intent intent)288     public boolean onUnbind(Intent intent) {
289         mClients.clear();
290 
291         return false;
292     }
293 }
294