1 /**
2  * Copyright (C) 2023 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 com.android.server.soundtrigger;
18 
19 import static android.os.PowerManager.SOUND_TRIGGER_MODE_ALL_DISABLED;
20 import static android.os.PowerManager.SOUND_TRIGGER_MODE_ALL_ENABLED;
21 import static android.os.PowerManager.SOUND_TRIGGER_MODE_CRITICAL_ONLY;
22 
23 import com.android.internal.annotations.GuardedBy;
24 import com.android.server.utils.EventLogger;
25 
26 import java.io.PrintWriter;
27 import java.util.HashSet;
28 import java.util.Objects;
29 import java.util.Set;
30 import java.util.concurrent.ConcurrentHashMap;
31 import java.util.concurrent.CountDownLatch;
32 import java.util.concurrent.Executor;
33 import java.util.concurrent.Executors;
34 import java.util.concurrent.TimeUnit;
35 
36 /**
37  * Manages device state events which require pausing SoundTrigger recognition
38  *
39  * @hide
40  */
41 public class DeviceStateHandler implements PhoneCallStateHandler.Callback {
42 
43     public static final long CALL_INACTIVE_MSG_DELAY_MS = 1000;
44 
45     public interface DeviceStateListener {
onSoundTriggerDeviceStateUpdate(SoundTriggerDeviceState state)46         void onSoundTriggerDeviceStateUpdate(SoundTriggerDeviceState state);
47     }
48 
49     public enum SoundTriggerDeviceState {
50         DISABLE, // The device state requires all SoundTrigger sessions are disabled
51         CRITICAL, // The device state requires all non-critical SoundTrigger sessions are disabled
52         ENABLE // The device state permits all SoundTrigger sessions
53     }
54 
55     private final Object mLock = new Object();
56 
57     private final EventLogger mEventLogger;
58 
59     @GuardedBy("mLock")
60     SoundTriggerDeviceState mSoundTriggerDeviceState = SoundTriggerDeviceState.ENABLE;
61 
62     // Individual components of the SoundTriggerDeviceState
63     @GuardedBy("mLock")
64     private int mSoundTriggerPowerSaveMode = SOUND_TRIGGER_MODE_ALL_ENABLED;
65 
66     @GuardedBy("mLock")
67     private boolean mIsPhoneCallOngoing = false;
68 
69     // There can only be one pending notify at any given time.
70     // If any phone state change comes in between, we will cancel the previous pending
71     // task.
72     @GuardedBy("mLock")
73     private NotificationTask mPhoneStateChangePendingNotify = null;
74 
75     private Set<DeviceStateListener> mCallbackSet = ConcurrentHashMap.newKeySet(4);
76 
77     private final Executor mDelayedNotificationExecutor = Executors.newSingleThreadExecutor();
78 
79     private final Executor mCallbackExecutor;
80 
onPowerModeChanged(int soundTriggerPowerSaveMode)81     public void onPowerModeChanged(int soundTriggerPowerSaveMode) {
82         mEventLogger.enqueue(new SoundTriggerPowerEvent(soundTriggerPowerSaveMode));
83         synchronized (mLock) {
84             if (soundTriggerPowerSaveMode == mSoundTriggerPowerSaveMode) {
85                 // No state change, nothing to do
86                 return;
87             }
88             mSoundTriggerPowerSaveMode = soundTriggerPowerSaveMode;
89             evaluateStateChange();
90         }
91     }
92 
93     @Override
onPhoneCallStateChanged(boolean isInPhoneCall)94     public void onPhoneCallStateChanged(boolean isInPhoneCall) {
95         mEventLogger.enqueue(new PhoneCallEvent(isInPhoneCall));
96         synchronized (mLock) {
97             if (mIsPhoneCallOngoing == isInPhoneCall) {
98                 // no change, nothing to do
99                 return;
100             }
101             // Clear any pending notification
102             if (mPhoneStateChangePendingNotify != null) {
103                 mPhoneStateChangePendingNotify.cancel();
104                 mPhoneStateChangePendingNotify = null;
105             }
106             mIsPhoneCallOngoing = isInPhoneCall;
107             if (!mIsPhoneCallOngoing) {
108                 // State has changed from call to no call, delay notification
109                 mPhoneStateChangePendingNotify = new NotificationTask(
110                         new Runnable() {
111                             @Override
112                             public void run() {
113                                 synchronized (mLock) {
114                                     if (mPhoneStateChangePendingNotify != null &&
115                                             mPhoneStateChangePendingNotify.runnableEquals(this)) {
116 
117                                         mPhoneStateChangePendingNotify = null;
118                                         evaluateStateChange();
119                                     }
120                                 }
121                             }
122                         },
123                         CALL_INACTIVE_MSG_DELAY_MS);
124                 mDelayedNotificationExecutor.execute(mPhoneStateChangePendingNotify);
125             } else {
126                 evaluateStateChange();
127             }
128         }
129     }
130 
131     /** Note, we expect initial callbacks immediately following construction */
DeviceStateHandler(Executor callbackExecutor, EventLogger eventLogger)132     public DeviceStateHandler(Executor callbackExecutor, EventLogger eventLogger) {
133         mCallbackExecutor = Objects.requireNonNull(callbackExecutor);
134         mEventLogger = Objects.requireNonNull(eventLogger);
135     }
136 
getDeviceState()137     public SoundTriggerDeviceState getDeviceState() {
138         synchronized (mLock) {
139             return mSoundTriggerDeviceState;
140         }
141     }
142 
registerListener(DeviceStateListener callback)143     public void registerListener(DeviceStateListener callback) {
144         final var state = getDeviceState();
145         mCallbackExecutor.execute(
146                 () -> callback.onSoundTriggerDeviceStateUpdate(state));
147         mCallbackSet.add(callback);
148     }
149 
unregisterListener(DeviceStateListener callback)150     public void unregisterListener(DeviceStateListener callback) {
151         mCallbackSet.remove(callback);
152     }
153 
dump(PrintWriter pw)154     void dump(PrintWriter pw) {
155         synchronized (mLock) {
156             pw.println("DeviceState: " + mSoundTriggerDeviceState.name());
157             pw.println("PhoneState: " + mIsPhoneCallOngoing);
158             pw.println("PowerSaveMode: " + mSoundTriggerPowerSaveMode);
159         }
160     }
161 
162     @GuardedBy("mLock")
evaluateStateChange()163     private void evaluateStateChange() {
164         // We should wait until any pending delays are complete to update.
165         // We will eventually get called by the notification task, or something which
166         // cancels it.
167         // Additionally, if there isn't a state change, there is nothing to update.
168         SoundTriggerDeviceState newState = computeState();
169         if (mPhoneStateChangePendingNotify != null || mSoundTriggerDeviceState == newState) {
170             return;
171         }
172 
173         mSoundTriggerDeviceState = newState;
174         mEventLogger.enqueue(new DeviceStateEvent(mSoundTriggerDeviceState));
175         final var state = mSoundTriggerDeviceState;
176         for (var callback : mCallbackSet) {
177             mCallbackExecutor.execute(
178                     () -> callback.onSoundTriggerDeviceStateUpdate(state));
179         }
180     }
181 
182     @GuardedBy("mLock")
computeState()183     private SoundTriggerDeviceState computeState() {
184         if (mIsPhoneCallOngoing) {
185             return SoundTriggerDeviceState.DISABLE;
186         }
187         return switch (mSoundTriggerPowerSaveMode) {
188             case SOUND_TRIGGER_MODE_ALL_ENABLED -> SoundTriggerDeviceState.ENABLE;
189             case SOUND_TRIGGER_MODE_CRITICAL_ONLY -> SoundTriggerDeviceState.CRITICAL;
190             case SOUND_TRIGGER_MODE_ALL_DISABLED -> SoundTriggerDeviceState.DISABLE;
191             default -> throw new IllegalStateException(
192                     "Received unexpected power state code" + mSoundTriggerPowerSaveMode);
193         };
194     }
195 
196     /**
197      * One-shot, cancellable task which runs after a delay. Run must only be called once, from a
198      * single thread. Cancel can be called from any other thread.
199      */
200     private static class NotificationTask implements Runnable {
201         private final Runnable mRunnable;
202         private final long mWaitInMillis;
203 
204         private final CountDownLatch mCancelLatch = new CountDownLatch(1);
205 
NotificationTask(Runnable r, long waitInMillis)206         NotificationTask(Runnable r, long waitInMillis) {
207             mRunnable = r;
208             mWaitInMillis = waitInMillis;
209         }
210 
cancel()211         void cancel() {
212             mCancelLatch.countDown();
213         }
214 
215         // Used for determining task equality.
runnableEquals(Runnable runnable)216         boolean runnableEquals(Runnable runnable) {
217             return mRunnable == runnable;
218         }
219 
run()220         public void run() {
221             try {
222                 if (!mCancelLatch.await(mWaitInMillis, TimeUnit.MILLISECONDS)) {
223                     mRunnable.run();
224                 }
225             } catch (InterruptedException e) {
226                 Thread.currentThread().interrupt();
227                 throw new AssertionError("Unexpected InterruptedException", e);
228             }
229         }
230     }
231 
232     private static class PhoneCallEvent extends EventLogger.Event {
233         final boolean mIsInPhoneCall;
234 
PhoneCallEvent(boolean isInPhoneCall)235         PhoneCallEvent(boolean isInPhoneCall) {
236             mIsInPhoneCall = isInPhoneCall;
237         }
238 
239         @Override
eventToString()240         public String eventToString() {
241             return "PhoneCallChange - inPhoneCall: " + mIsInPhoneCall;
242         }
243     }
244 
245     private static class SoundTriggerPowerEvent extends EventLogger.Event {
246         final int mSoundTriggerPowerState;
247 
SoundTriggerPowerEvent(int soundTriggerPowerState)248         SoundTriggerPowerEvent(int soundTriggerPowerState) {
249             mSoundTriggerPowerState = soundTriggerPowerState;
250         }
251 
252         @Override
eventToString()253         public String eventToString() {
254             return "SoundTriggerPowerChange: " + stateToString();
255         }
256 
stateToString()257         private String stateToString() {
258             return switch (mSoundTriggerPowerState) {
259                 case SOUND_TRIGGER_MODE_ALL_ENABLED -> "All enabled";
260                 case SOUND_TRIGGER_MODE_CRITICAL_ONLY -> "Critical only";
261                 case SOUND_TRIGGER_MODE_ALL_DISABLED -> "All disabled";
262                 default -> "Unknown power state: " + mSoundTriggerPowerState;
263             };
264         }
265     }
266 
267     private static class DeviceStateEvent extends EventLogger.Event {
268         final SoundTriggerDeviceState mSoundTriggerDeviceState;
269 
270         DeviceStateEvent(SoundTriggerDeviceState soundTriggerDeviceState) {
271             mSoundTriggerDeviceState = soundTriggerDeviceState;
272         }
273 
274         @Override
275         public String eventToString() {
276             return "DeviceStateChange: " + mSoundTriggerDeviceState.name();
277         }
278     }
279 }
280