diff options
5 files changed, 777 insertions, 191 deletions
diff --git a/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger/DeviceStateHandlerTest.java b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger/DeviceStateHandlerTest.java new file mode 100644 index 000000000000..089bd454bfb8 --- /dev/null +++ b/services/tests/voiceinteractiontests/src/com/android/server/soundtrigger/DeviceStateHandlerTest.java @@ -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); + } + } +} diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/DeviceStateHandler.java b/services/voiceinteraction/java/com/android/server/soundtrigger/DeviceStateHandler.java new file mode 100644 index 000000000000..66054494c277 --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/DeviceStateHandler.java @@ -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(); + } + } +} diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java b/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java new file mode 100644 index 000000000000..8773cabeeb92 --- /dev/null +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/PhoneCallStateHandler.java @@ -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); + }; + } +} diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java index 255db1e4db83..b4066ab1ff39 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java @@ -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 diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java index 913535e06a21..3151781ff7ba 100644 --- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java +++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java @@ -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(); } } |