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