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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user