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
17package android.media.soundtrigger;
18
19import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
20
21import android.annotation.CallSuper;
22import android.annotation.MainThread;
23import android.annotation.NonNull;
24import android.annotation.Nullable;
25import android.annotation.SystemApi;
26import android.app.Service;
27import android.content.Context;
28import android.content.Intent;
29import android.hardware.soundtrigger.SoundTrigger;
30import android.os.Bundle;
31import android.os.Handler;
32import android.os.IBinder;
33import android.os.ParcelUuid;
34import android.os.RemoteException;
35import android.util.ArrayMap;
36import android.util.Log;
37
38import com.android.internal.annotations.GuardedBy;
39
40import 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
66public 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
87    protected final void attachBaseContext(Context base) {
88        super.attachBaseContext(base);
89        mHandler = new Handler(base.getMainLooper());
90    }
91
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
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
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
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
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
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
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     */
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
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
288    public boolean onUnbind(Intent intent) {
289        mClients.clear();
290
291        return false;
292    }
293}
294