blob: 66054494c2774bbfcbc300ca747f937c2c156a52 [file] [log] [blame]
/**
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.soundtrigger;
import static android.os.PowerManager.SOUND_TRIGGER_MODE_ALL_DISABLED;
import static android.os.PowerManager.SOUND_TRIGGER_MODE_ALL_ENABLED;
import static android.os.PowerManager.SOUND_TRIGGER_MODE_CRITICAL_ONLY;
import com.android.internal.annotations.GuardedBy;
import com.android.server.utils.EventLogger;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
* Manages device state events which require pausing SoundTrigger recognition
*
* @hide
*/
public class DeviceStateHandler implements PhoneCallStateHandler.Callback {
public static final long CALL_INACTIVE_MSG_DELAY_MS = 1000;
public interface DeviceStateListener {
void onSoundTriggerDeviceStateUpdate(SoundTriggerDeviceState state);
}
public enum SoundTriggerDeviceState {
DISABLE, // The device state requires all SoundTrigger sessions are disabled
CRITICAL, // The device state requires all non-critical SoundTrigger sessions are disabled
ENABLE // The device state permits all SoundTrigger sessions
}
private final Object mLock = new Object();
private final EventLogger mEventLogger;
@GuardedBy("mLock")
SoundTriggerDeviceState mSoundTriggerDeviceState = SoundTriggerDeviceState.ENABLE;
// Individual components of the SoundTriggerDeviceState
@GuardedBy("mLock")
private int mSoundTriggerPowerSaveMode = SOUND_TRIGGER_MODE_ALL_ENABLED;
@GuardedBy("mLock")
private boolean mIsPhoneCallOngoing = false;
// There can only be one pending notify at any given time.
// If any phone state change comes in between, we will cancel the previous pending
// task.
@GuardedBy("mLock")
private NotificationTask mPhoneStateChangePendingNotify = null;
private Set<DeviceStateListener> mCallbackSet = ConcurrentHashMap.newKeySet(4);
private final Executor mDelayedNotificationExecutor = Executors.newSingleThreadExecutor();
private final Executor mCallbackExecutor;
public void onPowerModeChanged(int soundTriggerPowerSaveMode) {
mEventLogger.enqueue(new SoundTriggerPowerEvent(soundTriggerPowerSaveMode));
synchronized (mLock) {
if (soundTriggerPowerSaveMode == mSoundTriggerPowerSaveMode) {
// No state change, nothing to do
return;
}
mSoundTriggerPowerSaveMode = soundTriggerPowerSaveMode;
evaluateStateChange();
}
}
@Override
public void onPhoneCallStateChanged(boolean isInPhoneCall) {
mEventLogger.enqueue(new PhoneCallEvent(isInPhoneCall));
synchronized (mLock) {
if (mIsPhoneCallOngoing == isInPhoneCall) {
// no change, nothing to do
return;
}
// Clear any pending notification
if (mPhoneStateChangePendingNotify != null) {
mPhoneStateChangePendingNotify.cancel();
mPhoneStateChangePendingNotify = null;
}
mIsPhoneCallOngoing = isInPhoneCall;
if (!mIsPhoneCallOngoing) {
// State has changed from call to no call, delay notification
mPhoneStateChangePendingNotify = new NotificationTask(
new Runnable() {
@Override
public void run() {
synchronized (mLock) {
if (mPhoneStateChangePendingNotify != null &&
mPhoneStateChangePendingNotify.runnableEquals(this)) {
mPhoneStateChangePendingNotify = null;
evaluateStateChange();
}
}
}
},
CALL_INACTIVE_MSG_DELAY_MS);
mDelayedNotificationExecutor.execute(mPhoneStateChangePendingNotify);
} else {
evaluateStateChange();
}
}
}
/** Note, we expect initial callbacks immediately following construction */
public DeviceStateHandler(Executor callbackExecutor, EventLogger eventLogger) {
mCallbackExecutor = Objects.requireNonNull(callbackExecutor);
mEventLogger = Objects.requireNonNull(eventLogger);
}
public SoundTriggerDeviceState getDeviceState() {
synchronized (mLock) {
return mSoundTriggerDeviceState;
}
}
public void registerListener(DeviceStateListener callback) {
final var state = getDeviceState();
mCallbackExecutor.execute(
() -> callback.onSoundTriggerDeviceStateUpdate(state));
mCallbackSet.add(callback);
}
public void unregisterListener(DeviceStateListener callback) {
mCallbackSet.remove(callback);
}
void dump(PrintWriter pw) {
synchronized (mLock) {
pw.println("DeviceState: " + mSoundTriggerDeviceState.name());
pw.println("PhoneState: " + mIsPhoneCallOngoing);
pw.println("PowerSaveMode: " + mSoundTriggerPowerSaveMode);
}
}
@GuardedBy("mLock")
private void evaluateStateChange() {
// We should wait until any pending delays are complete to update.
// We will eventually get called by the notification task, or something which
// cancels it.
// Additionally, if there isn't a state change, there is nothing to update.
SoundTriggerDeviceState newState = computeState();
if (mPhoneStateChangePendingNotify != null || mSoundTriggerDeviceState == newState) {
return;
}
mSoundTriggerDeviceState = newState;
mEventLogger.enqueue(new DeviceStateEvent(mSoundTriggerDeviceState));
final var state = mSoundTriggerDeviceState;
for (var callback : mCallbackSet) {
mCallbackExecutor.execute(
() -> callback.onSoundTriggerDeviceStateUpdate(state));
}
}
@GuardedBy("mLock")
private SoundTriggerDeviceState computeState() {
if (mIsPhoneCallOngoing) {
return SoundTriggerDeviceState.DISABLE;
}
return switch (mSoundTriggerPowerSaveMode) {
case SOUND_TRIGGER_MODE_ALL_ENABLED -> SoundTriggerDeviceState.ENABLE;
case SOUND_TRIGGER_MODE_CRITICAL_ONLY -> SoundTriggerDeviceState.CRITICAL;
case SOUND_TRIGGER_MODE_ALL_DISABLED -> SoundTriggerDeviceState.DISABLE;
default -> throw new IllegalStateException(
"Received unexpected power state code" + mSoundTriggerPowerSaveMode);
};
}
/**
* One-shot, cancellable task which runs after a delay. Run must only be called once, from a
* single thread. Cancel can be called from any other thread.
*/
private static class NotificationTask implements Runnable {
private final Runnable mRunnable;
private final long mWaitInMillis;
private final CountDownLatch mCancelLatch = new CountDownLatch(1);
NotificationTask(Runnable r, long waitInMillis) {
mRunnable = r;
mWaitInMillis = waitInMillis;
}
void cancel() {
mCancelLatch.countDown();
}
// Used for determining task equality.
boolean runnableEquals(Runnable runnable) {
return mRunnable == runnable;
}
public void run() {
try {
if (!mCancelLatch.await(mWaitInMillis, TimeUnit.MILLISECONDS)) {
mRunnable.run();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new AssertionError("Unexpected InterruptedException", e);
}
}
}
private static class PhoneCallEvent extends EventLogger.Event {
final boolean mIsInPhoneCall;
PhoneCallEvent(boolean isInPhoneCall) {
mIsInPhoneCall = isInPhoneCall;
}
@Override
public String eventToString() {
return "PhoneCallChange - inPhoneCall: " + mIsInPhoneCall;
}
}
private static class SoundTriggerPowerEvent extends EventLogger.Event {
final int mSoundTriggerPowerState;
SoundTriggerPowerEvent(int soundTriggerPowerState) {
mSoundTriggerPowerState = soundTriggerPowerState;
}
@Override
public String eventToString() {
return "SoundTriggerPowerChange: " + stateToString();
}
private String stateToString() {
return switch (mSoundTriggerPowerState) {
case SOUND_TRIGGER_MODE_ALL_ENABLED -> "All enabled";
case SOUND_TRIGGER_MODE_CRITICAL_ONLY -> "Critical only";
case SOUND_TRIGGER_MODE_ALL_DISABLED -> "All disabled";
default -> "Unknown power state: " + mSoundTriggerPowerState;
};
}
}
private static class DeviceStateEvent extends EventLogger.Event {
final SoundTriggerDeviceState mSoundTriggerDeviceState;
DeviceStateEvent(SoundTriggerDeviceState soundTriggerDeviceState) {
mSoundTriggerDeviceState = soundTriggerDeviceState;
}
@Override
public String eventToString() {
return "DeviceStateChange: " + mSoundTriggerDeviceState.name();
}
}
}