SoundTrigger DeviceState

- Migrate ST device state listeners to be shared across the service,
  rather than being spawned on demand per-session. Each session
  registers callbacks against the global device state listener.
- Abstract phone state callbacks into separate handler since the
  new telephony callback interface requires registering listeners per
  subscription
- Retain the behavior of only resuming recognition 1s after phone call
  ends via posting a cancellable future
- Add EventLogger for device state events

Bug: 272147641
Fixes: 275105854
Fixes: 267960077
Test: AlwaysOnHotwordDetectionTest - battery state tests
Test: ""#testOnPhoneCall_recognitionPausedAndResumed
Test: Manual verification of battery saver toggle
Test: Manual verification of extreme battery saver toggle
Test: Manual verification of phone call
Test: DeviceStateHandlerTest
Change-Id: Iac5408e7734451fa9399fb09e08184db01b7f9d1
This commit is contained in:
Atneya Nair
2023-04-05 23:24:06 -07:00
parent 1030b6c63f
commit e239b59573
5 changed files with 777 additions and 191 deletions

View File

@@ -0,0 +1,261 @@
/*
* 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 static com.android.server.soundtrigger.DeviceStateHandler.SoundTriggerDeviceState;
import static com.android.server.soundtrigger.DeviceStateHandler.SoundTriggerDeviceState.*;
import static com.google.common.truth.Truth.assertThat;
import android.os.SystemClock;
import androidx.test.filters.FlakyTest;
import androidx.test.runner.AndroidJUnit4;
import com.android.internal.annotations.GuardedBy;
import com.android.server.utils.EventLogger;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
public final class DeviceStateHandlerTest {
private final long CONFIRM_NO_EVENT_WAIT_MS = 1000;
// A wait substantially less than the duration we delay phone notifications by
private final long PHONE_DELAY_BRIEF_WAIT_MS =
DeviceStateHandler.CALL_INACTIVE_MSG_DELAY_MS / 4;
private DeviceStateHandler mHandler;
private DeviceStateHandler.DeviceStateListener mDeviceStateCallback;
private final Object mLock = new Object();
@GuardedBy("mLock")
private SoundTriggerDeviceState mState;
@GuardedBy("mLock")
private CountDownLatch mLatch;
private EventLogger mEventLogger;
@Before
public void setup() {
// Reset the state prior to each test
mEventLogger = new EventLogger(256, "test logger");
synchronized (mLock) {
mLatch = new CountDownLatch(1);
}
mDeviceStateCallback =
(SoundTriggerDeviceState state) -> {
synchronized (mLock) {
mState = state;
mLatch.countDown();
}
};
mHandler = new DeviceStateHandler(Runnable::run, mEventLogger);
mHandler.onPhoneCallStateChanged(false);
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_ENABLED);
mHandler.registerListener(mDeviceStateCallback);
try {
waitAndAssertState(ENABLE);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private void waitAndAssertState(SoundTriggerDeviceState state) throws InterruptedException {
CountDownLatch latch;
synchronized (mLock) {
latch = mLatch;
}
latch.await();
synchronized (mLock) {
assertThat(mState).isEqualTo(state);
mLatch = new CountDownLatch(1);
}
}
private void waitToConfirmNoEventReceived() throws InterruptedException {
CountDownLatch latch;
synchronized (mLock) {
latch = mLatch;
}
// Check that we time out
assertThat(latch.await(CONFIRM_NO_EVENT_WAIT_MS, TimeUnit.MILLISECONDS)).isFalse();
}
@Test
public void onPowerModeChangedCritical_receiveStateChange() throws Exception {
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY);
waitAndAssertState(CRITICAL);
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_ENABLED);
waitAndAssertState(ENABLE);
}
@Test
public void onPowerModeChangedDisabled_receiveStateChange() throws Exception {
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_DISABLED);
waitAndAssertState(DISABLE);
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_ENABLED);
waitAndAssertState(ENABLE);
}
@Test
public void onPowerModeChangedMultiple_receiveStateChange() throws Exception {
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_DISABLED);
waitAndAssertState(DISABLE);
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY);
waitAndAssertState(CRITICAL);
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_DISABLED);
waitAndAssertState(DISABLE);
}
@Test
public void onPowerModeSameState_noStateChange() throws Exception {
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_DISABLED);
waitAndAssertState(DISABLE);
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_DISABLED);
waitToConfirmNoEventReceived();
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY);
waitAndAssertState(CRITICAL);
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY);
waitToConfirmNoEventReceived();
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_ENABLED);
waitAndAssertState(ENABLE);
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_ENABLED);
waitToConfirmNoEventReceived();
}
@Test
public void onPhoneCall_receiveStateChange() throws Exception {
mHandler.onPhoneCallStateChanged(true);
waitAndAssertState(DISABLE);
mHandler.onPhoneCallStateChanged(false);
waitAndAssertState(ENABLE);
}
@Test
public void onPhoneCall_receiveStateChangeIsDelayed() throws Exception {
mHandler.onPhoneCallStateChanged(true);
waitAndAssertState(DISABLE);
long beforeTime = SystemClock.uptimeMillis();
mHandler.onPhoneCallStateChanged(false);
waitAndAssertState(ENABLE);
long afterTime = SystemClock.uptimeMillis();
assertThat(afterTime - beforeTime).isAtLeast(DeviceStateHandler.CALL_INACTIVE_MSG_DELAY_MS);
}
@Test
public void onPhoneCallEnterExitEnter_receiveNoStateChange() throws Exception {
mHandler.onPhoneCallStateChanged(true);
waitAndAssertState(DISABLE);
mHandler.onPhoneCallStateChanged(false);
SystemClock.sleep(PHONE_DELAY_BRIEF_WAIT_MS);
mHandler.onPhoneCallStateChanged(true);
waitToConfirmNoEventReceived();
}
@Test
public void onBatteryCallbackDuringPhoneWait_receiveStateChangeDelayed() throws Exception {
mHandler.onPhoneCallStateChanged(true);
waitAndAssertState(DISABLE);
mHandler.onPhoneCallStateChanged(false);
SystemClock.sleep(PHONE_DELAY_BRIEF_WAIT_MS);
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY);
waitAndAssertState(CRITICAL);
// Ensure we don't get an ENABLE event after
waitToConfirmNoEventReceived();
}
@Test
public void onBatteryChangeWhenInPhoneCall_receiveNoStateChange() throws Exception {
mHandler.onPhoneCallStateChanged(true);
waitAndAssertState(DISABLE);
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_ENABLED);
waitToConfirmNoEventReceived();
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY);
waitToConfirmNoEventReceived();
}
@Test
public void whenBatteryCriticalChangeDuringCallAfterPhoneCall_receiveCriticalStateChange()
throws Exception {
mHandler.onPhoneCallStateChanged(true);
waitAndAssertState(DISABLE);
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY);
waitToConfirmNoEventReceived();
mHandler.onPhoneCallStateChanged(false);
waitAndAssertState(CRITICAL);
}
@Test
public void whenBatteryDisableDuringCallAfterPhoneCallBatteryEnable_receiveStateChange()
throws Exception {
mHandler.onPhoneCallStateChanged(true);
waitAndAssertState(DISABLE);
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_DISABLED);
waitToConfirmNoEventReceived();
mHandler.onPhoneCallStateChanged(false);
waitToConfirmNoEventReceived();
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY);
waitAndAssertState(CRITICAL);
}
@Test
public void whenPhoneCallDuringBatteryDisable_receiveNoStateChange() throws Exception {
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_ALL_DISABLED);
waitAndAssertState(DISABLE);
mHandler.onPhoneCallStateChanged(true);
waitToConfirmNoEventReceived();
mHandler.onPhoneCallStateChanged(false);
waitToConfirmNoEventReceived();
}
@Test
public void whenPhoneCallDuringBatteryCritical_receiveStateChange() throws Exception {
mHandler.onPowerModeChanged(SOUND_TRIGGER_MODE_CRITICAL_ONLY);
waitAndAssertState(CRITICAL);
mHandler.onPhoneCallStateChanged(true);
waitAndAssertState(DISABLE);
mHandler.onPhoneCallStateChanged(false);
waitAndAssertState(CRITICAL);
}
// This test could be flaky, but we want to verify that we only delay notification if
// we are exiting a call, NOT if we are entering a call.
@FlakyTest
@Test
public void whenPhoneCallReceived_receiveStateChangeFast() throws Exception {
mHandler.onPhoneCallStateChanged(true);
CountDownLatch latch;
synchronized (mLock) {
latch = mLatch;
}
assertThat(latch.await(PHONE_DELAY_BRIEF_WAIT_MS, TimeUnit.MILLISECONDS)).isTrue();
synchronized (mLock) {
assertThat(mState).isEqualTo(DISABLE);
}
}
}

View File

@@ -0,0 +1,279 @@
/**
* 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();
}
}
}

View File

@@ -0,0 +1,158 @@
/**
* 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 android.telephony.Annotation;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyCallback;
import android.telephony.TelephonyManager;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Handles monitoring telephony call state across active subscriptions.
*
* @hide
*/
public class PhoneCallStateHandler {
public interface Callback {
void onPhoneCallStateChanged(boolean isInPhoneCall);
}
private final Object mLock = new Object();
// Actually never contended due to executor.
@GuardedBy("mLock")
private final List<MyCallStateListener> mListenerList = new ArrayList<>();
private final AtomicBoolean mIsPhoneCallOngoing = new AtomicBoolean(false);
private final SubscriptionManager mSubscriptionManager;
private final TelephonyManager mTelephonyManager;
private final Callback mCallback;
private final ExecutorService mExecutor = Executors.newSingleThreadExecutor();
public PhoneCallStateHandler(
SubscriptionManager subscriptionManager,
TelephonyManager telephonyManager,
Callback callback) {
mSubscriptionManager = Objects.requireNonNull(subscriptionManager);
mTelephonyManager = Objects.requireNonNull(telephonyManager);
mCallback = Objects.requireNonNull(callback);
mSubscriptionManager.addOnSubscriptionsChangedListener(
mExecutor,
new SubscriptionManager.OnSubscriptionsChangedListener() {
@Override
public void onSubscriptionsChanged() {
updateTelephonyListeners();
}
@Override
public void onAddListenerFailed() {
Slog.wtf(
"SoundTriggerPhoneCallStateHandler",
"Failed to add a telephony listener");
}
});
}
private final class MyCallStateListener extends TelephonyCallback
implements TelephonyCallback.CallStateListener {
final TelephonyManager mTelephonyManagerForSubId;
// Manager corresponding to the sub-id
MyCallStateListener(TelephonyManager telephonyManager) {
mTelephonyManagerForSubId = telephonyManager;
}
void cleanup() {
mExecutor.execute(() -> mTelephonyManagerForSubId.unregisterTelephonyCallback(this));
}
@Override
public void onCallStateChanged(int unused) {
updateCallStatus();
}
}
/** Compute the current call status, and dispatch callback if it has changed. */
private void updateCallStatus() {
boolean callStatus = checkCallStatus();
if (mIsPhoneCallOngoing.compareAndSet(!callStatus, callStatus)) {
mCallback.onPhoneCallStateChanged(callStatus);
}
}
/**
* Synchronously query the current telephony call state across all subscriptions
*
* @return - {@code true} if in call, {@code false} if not in call.
*/
private boolean checkCallStatus() {
List<SubscriptionInfo> infoList = mSubscriptionManager.getActiveSubscriptionInfoList();
if (infoList == null) return false;
return infoList.stream()
.filter(s -> (s.getSubscriptionId() != SubscriptionManager.INVALID_SUBSCRIPTION_ID))
.anyMatch(s -> isCallOngoingFromState(
mTelephonyManager
.createForSubscriptionId(s.getSubscriptionId())
.getCallStateForSubscription()));
}
private void updateTelephonyListeners() {
synchronized (mLock) {
for (var listener : mListenerList) {
listener.cleanup();
}
mListenerList.clear();
List<SubscriptionInfo> infoList = mSubscriptionManager.getActiveSubscriptionInfoList();
if (infoList == null) return;
infoList.stream()
.filter(s -> s.getSubscriptionId()
!= SubscriptionManager.INVALID_SUBSCRIPTION_ID)
.map(s -> mTelephonyManager.createForSubscriptionId(s.getSubscriptionId()))
.forEach(manager -> {
synchronized (mLock) {
var listener = new MyCallStateListener(manager);
mListenerList.add(listener);
manager.registerTelephonyCallback(mExecutor, listener);
}
});
}
}
private static boolean isCallOngoingFromState(@Annotation.CallState int callState) {
return switch (callState) {
case TelephonyManager.CALL_STATE_IDLE, TelephonyManager.CALL_STATE_RINGING -> false;
case TelephonyManager.CALL_STATE_OFFHOOK -> true;
default -> throw new IllegalStateException(
"Received unexpected call state from Telephony Manager: " + callState);
};
}
}

View File

@@ -16,15 +16,12 @@
package com.android.server.soundtrigger;
import static com.android.server.soundtrigger.DeviceStateHandler.SoundTriggerDeviceState;
import static com.android.server.soundtrigger.SoundTriggerEvent.SessionEvent.Type;
import static com.android.server.utils.EventLogger.Event.ALOGW;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.soundtrigger.IRecognitionStatusCallback;
import android.hardware.soundtrigger.ModelParams;
import android.hardware.soundtrigger.SoundTrigger;
@@ -45,11 +42,7 @@ import android.os.DeadObjectException;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.os.PowerManager.SoundTriggerPowerSaveMode;
import android.os.RemoteException;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
@@ -99,37 +92,20 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
private SoundTriggerModule mModule;
private final Object mLock = new Object();
private final Context mContext;
private final TelephonyManager mTelephonyManager;
private final PhoneStateListener mPhoneStateListener;
private final PowerManager mPowerManager;
// The SoundTriggerManager layer handles multiple recognition models of type generic and
// keyphrase. We store the ModelData here in a hashmap.
private final HashMap<UUID, ModelData> mModelDataMap;
private final HashMap<UUID, ModelData> mModelDataMap = new HashMap<>();
// An index of keyphrase sound models so that we can reach them easily. We support indexing
// keyphrase sound models with a keyphrase ID. Sound model with the same keyphrase ID will
// replace an existing model, thus there is a 1:1 mapping from keyphrase ID to a voice
// sound model.
private HashMap<Integer, UUID> mKeyphraseUuidMap;
private boolean mCallActive = false;
private @SoundTriggerPowerSaveMode int mSoundTriggerPowerSaveMode =
PowerManager.SOUND_TRIGGER_MODE_ALL_ENABLED;
private final HashMap<Integer, UUID> mKeyphraseUuidMap = new HashMap<>();
// Whether ANY recognition (keyphrase or generic) has been requested.
private boolean mRecognitionRequested = false;
private PowerSaveModeListener mPowerSaveModeListener;
// Handler to process call state changes will delay to allow time for the audio
// and sound trigger HALs to process the end of call notifications
// before we re enable pending recognition requests.
private final Handler mHandler;
private static final int MSG_CALL_STATE_CHANGED = 0;
private static final int CALL_INACTIVE_MSG_DELAY_MS = 1000;
// TODO(b/269366605) Temporary solution to query correct moduleProperties
private final int mModuleId;
private final Function<SoundTrigger.StatusListener, SoundTriggerModule> mModuleProvider;
@@ -139,16 +115,15 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
@GuardedBy("mLock")
private boolean mIsDetached = false;
@GuardedBy("mLock")
private SoundTriggerDeviceState mDeviceState = SoundTriggerDeviceState.DISABLE;
SoundTriggerHelper(Context context, EventLogger eventLogger,
@NonNull Function<SoundTrigger.StatusListener, SoundTriggerModule> moduleProvider,
int moduleId,
@NonNull Supplier<List<ModuleProperties>> modulePropertiesProvider) {
mModuleId = moduleId;
mContext = context;
mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
mPowerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
mModelDataMap = new HashMap<UUID, ModelData>();
mKeyphraseUuidMap = new HashMap<Integer, UUID>();
mModuleProvider = moduleProvider;
mEventLogger = eventLogger;
mModulePropertiesProvider = modulePropertiesProvider;
@@ -157,31 +132,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
} else {
mModule = mModuleProvider.apply(this);
}
Looper looper = Looper.myLooper();
if (looper == null) {
looper = Looper.getMainLooper();
}
mPhoneStateListener = new MyCallStateListener(looper);
if (looper != null) {
mHandler = new Handler(looper) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_CALL_STATE_CHANGED:
synchronized (mLock) {
onCallStateChangedLocked(
TelephonyManager.CALL_STATE_OFFHOOK == msg.arg1);
}
break;
default:
Slog.e(TAG, "unknown message in handler:" + msg.what);
break;
}
}
};
} else {
mHandler = null;
}
}
/**
@@ -373,7 +323,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
modelData.setSoundModel(soundModel);
if (!isRecognitionAllowedByDeviceState(modelData)) {
initializeDeviceStateListeners();
return STATUS_OK;
}
@@ -497,11 +446,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
modelData.setLoaded();
modelData.clearCallback();
modelData.setRecognitionConfig(null);
if (!computeRecognitionRequestedLocked()) {
internalClearGlobalStateLocked();
}
return status;
}
}
@@ -638,6 +582,17 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
}
}
public void onDeviceStateChanged(SoundTriggerDeviceState state) {
synchronized (mLock) {
if (mIsDetached || mDeviceState == state) {
// Nothing to update
return;
}
mDeviceState = state;
updateAllRecognitionsLocked();
}
}
public int getGenericModelState(UUID modelId) {
synchronized (mLock) {
MetricsLogger.count(mContext, "sth_get_generic_model_state", 1);
@@ -880,25 +835,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
}
}
private void onCallStateChangedLocked(boolean callActive) {
if (mCallActive == callActive) {
// We consider multiple call states as being active
// so we check if something really changed or not here.
return;
}
mCallActive = callActive;
updateAllRecognitionsLocked();
}
private void onPowerSaveModeChangedLocked(
@SoundTriggerPowerSaveMode int soundTriggerPowerSaveMode) {
if (mSoundTriggerPowerSaveMode == soundTriggerPowerSaveMode) {
return;
}
mSoundTriggerPowerSaveMode = soundTriggerPowerSaveMode;
updateAllRecognitionsLocked();
}
private void onModelUnloadedLocked(int modelHandle) {
ModelData modelData = getModelDataForLocked(modelHandle);
if (modelData != null) {
@@ -1011,10 +947,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
return status;
}
status = startRecognitionLocked(model, notifyClientOnError);
// Initialize power save, call active state monitoring logic.
if (status == STATUS_OK) {
initializeDeviceStateListeners();
}
return status;
} else {
return stopRecognitionLocked(model, notifyClientOnError);
@@ -1040,7 +972,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
}
} finally {
internalClearModelStateLocked();
internalClearGlobalStateLocked();
if (mModule != null) {
mModule.detach();
try {
@@ -1054,24 +985,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
}
}
// internalClearGlobalStateLocked() cleans up the telephony and power save listeners.
private void internalClearGlobalStateLocked() {
// Unregister from call state changes.
final long token = Binder.clearCallingIdentity();
try {
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
} finally {
Binder.restoreCallingIdentity(token);
}
// Unregister from power save mode changes.
if (mPowerSaveModeListener != null) {
mContext.unregisterReceiver(mPowerSaveModeListener);
mPowerSaveModeListener = null;
}
mRecognitionRequested = false;
}
// Clears state for all models (generic and keyphrase).
private void internalClearModelStateLocked() {
for (ModelData modelData : mModelDataMap.values()) {
@@ -1079,67 +992,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
}
}
class MyCallStateListener extends PhoneStateListener {
MyCallStateListener(@NonNull Looper looper) {
super(Objects.requireNonNull(looper));
}
@Override
public void onCallStateChanged(int state, String arg1) {
if (mHandler != null) {
synchronized (mLock) {
mHandler.removeMessages(MSG_CALL_STATE_CHANGED);
Message msg = mHandler.obtainMessage(MSG_CALL_STATE_CHANGED, state, 0);
mHandler.sendMessageDelayed(
msg, (TelephonyManager.CALL_STATE_OFFHOOK == state) ? 0
: CALL_INACTIVE_MSG_DELAY_MS);
}
}
}
}
class PowerSaveModeListener extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (!PowerManager.ACTION_POWER_SAVE_MODE_CHANGED.equals(intent.getAction())) {
return;
}
@SoundTriggerPowerSaveMode int soundTriggerPowerSaveMode =
mPowerManager.getSoundTriggerPowerSaveMode();
synchronized (mLock) {
onPowerSaveModeChangedLocked(soundTriggerPowerSaveMode);
}
}
}
private void initializeDeviceStateListeners() {
if (mRecognitionRequested) {
return;
}
final long token = Binder.clearCallingIdentity();
try {
// Get the current call state synchronously for the first recognition.
mCallActive = mTelephonyManager.getCallState() == TelephonyManager.CALL_STATE_OFFHOOK;
// Register for call state changes when the first call to start recognition occurs.
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
// Register for power saver mode changes when the first call to start recognition
// occurs.
if (mPowerSaveModeListener == null) {
mPowerSaveModeListener = new PowerSaveModeListener();
mContext.registerReceiver(mPowerSaveModeListener,
new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED));
}
mSoundTriggerPowerSaveMode = mPowerManager.getSoundTriggerPowerSaveMode();
mRecognitionRequested = true;
} finally {
Binder.restoreCallingIdentity(token);
}
}
/**
* Stops and unloads all models. This is intended as a clean-up call with the expectation that
* this instance is not used after.
@@ -1153,7 +1005,6 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
forceStopAndUnloadModelLocked(model, null);
}
mModelDataMap.clear();
internalClearGlobalStateLocked();
if (mModule != null) {
mModule.detach();
mModule = null;
@@ -1305,28 +1156,14 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
* @param modelData Model data to be used for recognition
* @return True if recognition is allowed to run at this time. False if not.
*/
@GuardedBy("mLock")
private boolean isRecognitionAllowedByDeviceState(ModelData modelData) {
// if mRecognitionRequested is false, call and power state listeners are not registered so
// we read current state directly from services
if (!mRecognitionRequested) {
mCallActive = mTelephonyManager.getCallState() == TelephonyManager.CALL_STATE_OFFHOOK;
mSoundTriggerPowerSaveMode = mPowerManager.getSoundTriggerPowerSaveMode();
}
return !mCallActive && isRecognitionAllowedByPowerState(
modelData);
}
/**
* Helper function to validate if a recognition should run based on the current power state
*
* @param modelData Model data to be used for recognition
* @return True if device state allows recognition to run, false if not.
*/
private boolean isRecognitionAllowedByPowerState(ModelData modelData) {
return mSoundTriggerPowerSaveMode == PowerManager.SOUND_TRIGGER_MODE_ALL_ENABLED
|| (mSoundTriggerPowerSaveMode == PowerManager.SOUND_TRIGGER_MODE_CRITICAL_ONLY
&& modelData.shouldRunInBatterySaverMode());
return switch (mDeviceState) {
case DISABLE -> false;
case CRITICAL -> modelData.shouldRunInBatterySaverMode();
case ENABLE -> true;
default -> throw new AssertionError("Enum changed between compile and runtime");
};
}
// A single routine that implements the start recognition logic for both generic and keyphrase

View File

@@ -35,14 +35,18 @@ import static com.android.server.soundtrigger.SoundTriggerEvent.SessionEvent.Typ
import static com.android.server.utils.EventLogger.Event.ALOGW;
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
import static com.android.server.soundtrigger.DeviceStateHandler.DeviceStateListener;
import static com.android.server.soundtrigger.DeviceStateHandler.SoundTriggerDeviceState;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.PermissionChecker;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
@@ -85,6 +89,8 @@ import android.os.ServiceSpecificException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.SparseArray;
@@ -112,6 +118,8 @@ import java.util.Objects;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@@ -131,8 +139,8 @@ public class SoundTriggerService extends SystemService {
private static final boolean DEBUG = true;
private static final int SESSION_MAX_EVENT_SIZE = 128;
final Context mContext;
private Object mLock;
private final Context mContext;
private final Object mLock = new Object();
private final SoundTriggerServiceStub mServiceStub;
private final LocalSoundTriggerService mLocalSoundTriggerService;
@@ -140,6 +148,7 @@ public class SoundTriggerService extends SystemService {
private SoundTriggerDbHelper mDbHelper;
private final EventLogger mServiceEventLogger = new EventLogger(256, "Service");
private final EventLogger mDeviceEventLogger = new EventLogger(256, "Device Event");
private final Set<EventLogger> mSessionEventLoggers = ConcurrentHashMap.newKeySet(4);
private final Deque<EventLogger> mDetachedSessionEventLoggers = new LinkedBlockingDeque<>(4);
@@ -223,13 +232,18 @@ public class SoundTriggerService extends SystemService {
@GuardedBy("mLock")
private final ArrayMap<String, NumOps> mNumOpsPerPackage = new ArrayMap<>();
private final DeviceStateHandler mDeviceStateHandler;
private final Executor mDeviceStateHandlerExecutor = Executors.newSingleThreadExecutor();
private PhoneCallStateHandler mPhoneCallStateHandler;
public SoundTriggerService(Context context) {
super(context);
mContext = context;
mServiceStub = new SoundTriggerServiceStub();
mLocalSoundTriggerService = new LocalSoundTriggerService(context);
mLock = new Object();
mSoundModelStatTracker = new SoundModelStatTracker();
mDeviceStateHandler = new DeviceStateHandler(mDeviceStateHandlerExecutor,
mDeviceEventLogger);
}
@Override
@@ -243,6 +257,29 @@ public class SoundTriggerService extends SystemService {
Slog.d(TAG, "onBootPhase: " + phase + " : " + isSafeMode());
if (PHASE_THIRD_PARTY_APPS_CAN_START == phase) {
mDbHelper = new SoundTriggerDbHelper(mContext);
final PowerManager powerManager = mContext.getSystemService(PowerManager.class);
// Hook up power state listener
mContext.registerReceiver(
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (!PowerManager.ACTION_POWER_SAVE_MODE_CHANGED
.equals(intent.getAction())) {
return;
}
mDeviceStateHandler.onPowerModeChanged(
powerManager.getSoundTriggerPowerSaveMode());
}
}, new IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED));
// Initialize the initial power state
// Do so after registering the listener so we ensure that we don't drop any events
mDeviceStateHandler.onPowerModeChanged(powerManager.getSoundTriggerPowerSaveMode());
// PhoneCallStateHandler initializes the original call state
mPhoneCallStateHandler = new PhoneCallStateHandler(
mContext.getSystemService(SubscriptionManager.class),
mContext.getSystemService(TelephonyManager.class),
mDeviceStateHandler);
}
mMiddlewareService = ISoundTriggerMiddlewareService.Stub.asInterface(
ServiceManager.waitForService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE));
@@ -380,6 +417,9 @@ public class SoundTriggerService extends SystemService {
// Event loggers
pw.println("##Service-Wide logs:");
mServiceEventLogger.dump(pw, /* indent = */ " ");
pw.println("\n##Device state logs:");
mDeviceStateHandler.dump(pw);
mDeviceEventLogger.dump(pw, /* indent = */ " ");
pw.println("\n##Active Session dumps:\n");
for (var sessionLogger : mSessionEventLoggers) {
@@ -403,6 +443,7 @@ public class SoundTriggerService extends SystemService {
class SoundTriggerSessionStub extends ISoundTriggerSession.Stub {
private final SoundTriggerHelper mSoundTriggerHelper;
private final DeviceStateListener mListener;
// Used to detect client death.
private final IBinder mClient;
private final Identity mOriginatorIdentity;
@@ -424,6 +465,9 @@ public class SoundTriggerService extends SystemService {
} catch (RemoteException e) {
clientDied();
}
mListener = (SoundTriggerDeviceState state)
-> mSoundTriggerHelper.onDeviceStateChanged(state);
mDeviceStateHandler.registerListener(mListener);
}
@Override
@@ -874,6 +918,7 @@ public class SoundTriggerService extends SystemService {
}
private void detach() {
mDeviceStateHandler.unregisterListener(mListener);
mSoundTriggerHelper.detach();
detachSessionLogger(mEventLogger);
}
@@ -890,7 +935,8 @@ public class SoundTriggerService extends SystemService {
private void enforceDetectionPermissions(ComponentName detectionService) {
PackageManager packageManager = mContext.getPackageManager();
String packageName = detectionService.getPackageName();
if (packageManager.checkPermission(Manifest.permission.CAPTURE_AUDIO_HOTWORD, packageName)
if (packageManager.checkPermission(
Manifest.permission.CAPTURE_AUDIO_HOTWORD, packageName)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException(detectionService.getPackageName() + " does not have"
+ " permission " + Manifest.permission.CAPTURE_AUDIO_HOTWORD);
@@ -1576,6 +1622,7 @@ public class SoundTriggerService extends SystemService {
private final @NonNull IBinder mClient;
private final EventLogger mEventLogger;
private final Identity mOriginatorIdentity;
private final @NonNull DeviceStateListener mListener;
private final SparseArray<UUID> mModelUuid = new SparseArray<>(1);
@@ -1594,6 +1641,9 @@ public class SoundTriggerService extends SystemService {
} catch (RemoteException e) {
clientDied();
}
mListener = (SoundTriggerDeviceState state)
-> mSoundTriggerHelper.onDeviceStateChanged(state);
mDeviceStateHandler.registerListener(mListener);
}
@Override
@@ -1662,6 +1712,7 @@ public class SoundTriggerService extends SystemService {
private void detachInternal() {
mEventLogger.enqueue(new SessionEvent(Type.DETACH, null));
detachSessionLogger(mEventLogger);
mDeviceStateHandler.unregisterListener(mListener);
mSoundTriggerHelper.detach();
}
}