Add AppOps listening to STService
Listen for RECORD AppOp revocations for SoundTrigger clients,
pause recognition if they enter MODE_IGNORED, and resume recognition
after MODE_GRANTED.
Bug: 272147641
Fixes: 267960430
Fixes: 274960506
Test: AlwaysOnHotwordDetectorTest#
testAppOpsLostReacquired_recognitionPausedResumed
Test: Manual verification of pause/resume flow when mic blocked
Test: Manual verification of pause/resume when booted w/ mic blocked
Change-Id: Ie907a46c9503ec163d466914f84e34c494ffd8d7
This commit is contained in:
@@ -119,6 +119,9 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
@GuardedBy("mLock")
|
||||
private SoundTriggerDeviceState mDeviceState = SoundTriggerDeviceState.DISABLE;
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private boolean mIsAppOpPermitted = true;
|
||||
|
||||
SoundTriggerHelper(Context context, EventLogger eventLogger,
|
||||
@NonNull Function<SoundTrigger.StatusListener, SoundTriggerModule> moduleProvider,
|
||||
int moduleId,
|
||||
@@ -323,7 +326,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
modelData.setRunInBatterySaverMode(runInBatterySaverMode);
|
||||
modelData.setSoundModel(soundModel);
|
||||
|
||||
if (isRecognitionAllowedByDeviceState(modelData)) {
|
||||
if (isRecognitionAllowed(modelData)) {
|
||||
int startRecoResult = updateRecognitionLocked(modelData,
|
||||
false /* Don't notify for synchronous calls */);
|
||||
if (startRecoResult == SoundTrigger.STATUS_OK) {
|
||||
@@ -613,6 +616,16 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
}
|
||||
}
|
||||
|
||||
public void onAppOpStateChanged(boolean isPermitted) {
|
||||
synchronized (mLock) {
|
||||
if (mIsAppOpPermitted == isPermitted) {
|
||||
return;
|
||||
}
|
||||
mIsAppOpPermitted = isPermitted;
|
||||
updateAllRecognitionsLocked();
|
||||
}
|
||||
}
|
||||
|
||||
public int getGenericModelState(UUID modelId) {
|
||||
synchronized (mLock) {
|
||||
MetricsLogger.count(mContext, "sth_get_generic_model_state", 1);
|
||||
@@ -782,6 +795,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
return event instanceof KeyphraseRecognitionEvent;
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void onGenericRecognitionLocked(GenericRecognitionEvent event) {
|
||||
MetricsLogger.count(mContext, "sth_generic_recognition_event", 1);
|
||||
if (event.status != SoundTrigger.RECOGNITION_STATUS_SUCCESS
|
||||
@@ -866,6 +880,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void onResourcesAvailableLocked() {
|
||||
mEventLogger.enqueue(new SessionEvent(Type.RESOURCES_AVAILABLE, null));
|
||||
updateAllRecognitionsLocked();
|
||||
@@ -911,6 +926,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
return keyphraseExtras[0].id;
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void onKeyphraseRecognitionLocked(KeyphraseRecognitionEvent event) {
|
||||
Slog.i(TAG, "Recognition success");
|
||||
MetricsLogger.count(mContext, "sth_keyphrase_recognition_event", 1);
|
||||
@@ -956,6 +972,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private void updateAllRecognitionsLocked() {
|
||||
// updateRecognitionLocked can possibly update the list of models
|
||||
ArrayList<ModelData> modelDatas = new ArrayList<ModelData>(mModelDataMap.values());
|
||||
@@ -964,8 +981,9 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mLock")
|
||||
private int updateRecognitionLocked(ModelData model, boolean notifyClientOnError) {
|
||||
boolean shouldStartModel = model.isRequested() && isRecognitionAllowedByDeviceState(model);
|
||||
boolean shouldStartModel = model.isRequested() && isRecognitionAllowed(model);
|
||||
if (shouldStartModel == model.isModelStarted()) {
|
||||
// No-op.
|
||||
return STATUS_OK;
|
||||
@@ -1184,7 +1202,10 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
* @return True if recognition is allowed to run at this time. False if not.
|
||||
*/
|
||||
@GuardedBy("mLock")
|
||||
private boolean isRecognitionAllowedByDeviceState(ModelData modelData) {
|
||||
private boolean isRecognitionAllowed(ModelData modelData) {
|
||||
if (!mIsAppOpPermitted) {
|
||||
return false;
|
||||
}
|
||||
return switch (mDeviceState) {
|
||||
case DISABLE -> false;
|
||||
case CRITICAL -> modelData.shouldRunInBatterySaverMode();
|
||||
@@ -1195,6 +1216,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
|
||||
// A single routine that implements the start recognition logic for both generic and keyphrase
|
||||
// models.
|
||||
@GuardedBy("mLock")
|
||||
private int startRecognitionLocked(ModelData modelData, boolean notifyClientOnError) {
|
||||
IRecognitionStatusCallback callback = modelData.getCallback();
|
||||
RecognitionConfig config = modelData.getRecognitionConfig();
|
||||
@@ -1205,7 +1227,7 @@ public class SoundTriggerHelper implements SoundTrigger.StatusListener {
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
|
||||
if (!isRecognitionAllowedByDeviceState(modelData)) {
|
||||
if (!isRecognitionAllowed(modelData)) {
|
||||
// Nothing to do here.
|
||||
Slog.w(TAG, "startRecognition requested but not allowed.");
|
||||
MetricsLogger.count(mContext, "sth_start_recognition_not_allowed", 1);
|
||||
|
||||
@@ -42,6 +42,7 @@ import android.Manifest;
|
||||
import android.annotation.NonNull;
|
||||
import android.annotation.Nullable;
|
||||
import android.app.ActivityThread;
|
||||
import android.app.AppOpsManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
@@ -110,6 +111,7 @@ import java.io.FileDescriptor;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.Deque;
|
||||
@@ -235,6 +237,8 @@ public class SoundTriggerService extends SystemService {
|
||||
private final DeviceStateHandler mDeviceStateHandler;
|
||||
private final Executor mDeviceStateHandlerExecutor = Executors.newSingleThreadExecutor();
|
||||
private PhoneCallStateHandler mPhoneCallStateHandler;
|
||||
private AppOpsManager mAppOpsManager;
|
||||
private PackageManager mPackageManager;
|
||||
|
||||
public SoundTriggerService(Context context) {
|
||||
super(context);
|
||||
@@ -257,6 +261,8 @@ public class SoundTriggerService extends SystemService {
|
||||
Slog.d(TAG, "onBootPhase: " + phase + " : " + isSafeMode());
|
||||
if (PHASE_THIRD_PARTY_APPS_CAN_START == phase) {
|
||||
mDbHelper = new SoundTriggerDbHelper(mContext);
|
||||
mAppOpsManager = mContext.getSystemService(AppOpsManager.class);
|
||||
mPackageManager = mContext.getPackageManager();
|
||||
final PowerManager powerManager = mContext.getSystemService(PowerManager.class);
|
||||
// Hook up power state listener
|
||||
mContext.registerReceiver(
|
||||
@@ -348,6 +354,44 @@ public class SoundTriggerService extends SystemService {
|
||||
}
|
||||
}
|
||||
|
||||
class MyAppOpsListener implements AppOpsManager.OnOpChangedListener {
|
||||
private final Identity mOriginatorIdentity;
|
||||
private final Consumer<Boolean> mOnOpModeChanged;
|
||||
|
||||
MyAppOpsListener(Identity originatorIdentity, Consumer<Boolean> onOpModeChanged) {
|
||||
mOriginatorIdentity = Objects.requireNonNull(originatorIdentity);
|
||||
mOnOpModeChanged = Objects.requireNonNull(onOpModeChanged);
|
||||
// Validate package name
|
||||
try {
|
||||
int uid = mPackageManager.getPackageUid(mOriginatorIdentity.packageName,
|
||||
PackageManager.PackageInfoFlags.of(0));
|
||||
if (uid != mOriginatorIdentity.uid) {
|
||||
throw new SecurityException("Package name: " +
|
||||
mOriginatorIdentity.packageName + "with uid: " + uid
|
||||
+ "attempted to spoof as: " + mOriginatorIdentity.uid);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new SecurityException("Package name not found: "
|
||||
+ mOriginatorIdentity.packageName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpChanged(String op, String packageName) {
|
||||
if (!Objects.equals(op, AppOpsManager.OPSTR_RECORD_AUDIO)) {
|
||||
return;
|
||||
}
|
||||
final int mode = mAppOpsManager.checkOpNoThrow(
|
||||
AppOpsManager.OPSTR_RECORD_AUDIO, mOriginatorIdentity.uid,
|
||||
mOriginatorIdentity.packageName);
|
||||
mOnOpModeChanged.accept(mode == AppOpsManager.MODE_ALLOWED);
|
||||
}
|
||||
|
||||
void forceOpChangeRefresh() {
|
||||
onOpChanged(AppOpsManager.OPSTR_RECORD_AUDIO, mOriginatorIdentity.packageName);
|
||||
}
|
||||
}
|
||||
|
||||
class SoundTriggerServiceStub extends ISoundTriggerService.Stub {
|
||||
@Override
|
||||
public ISoundTriggerSession attachAsOriginator(@NonNull Identity originatorIdentity,
|
||||
@@ -461,6 +505,7 @@ public class SoundTriggerService extends SystemService {
|
||||
private final Object mCallbacksLock = new Object();
|
||||
private final TreeMap<UUID, IRecognitionStatusCallback> mCallbacks = new TreeMap<>();
|
||||
private final EventLogger mEventLogger;
|
||||
private final MyAppOpsListener mAppOpsListener;
|
||||
|
||||
SoundTriggerSessionStub(@NonNull IBinder client,
|
||||
SoundTriggerHelper soundTriggerHelper, EventLogger eventLogger) {
|
||||
@@ -477,6 +522,12 @@ public class SoundTriggerService extends SystemService {
|
||||
}
|
||||
mListener = (SoundTriggerDeviceState state)
|
||||
-> mSoundTriggerHelper.onDeviceStateChanged(state);
|
||||
mAppOpsListener = new MyAppOpsListener(mOriginatorIdentity,
|
||||
mSoundTriggerHelper::onAppOpStateChanged);
|
||||
mAppOpsListener.forceOpChangeRefresh();
|
||||
mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_RECORD_AUDIO,
|
||||
mOriginatorIdentity.packageName, AppOpsManager.WATCH_FOREGROUND_CHANGES,
|
||||
mAppOpsListener);
|
||||
mDeviceStateHandler.registerListener(mListener);
|
||||
}
|
||||
|
||||
@@ -928,6 +979,9 @@ public class SoundTriggerService extends SystemService {
|
||||
}
|
||||
|
||||
private void detach() {
|
||||
if (mAppOpsListener != null) {
|
||||
mAppOpsManager.stopWatchingMode(mAppOpsListener);
|
||||
}
|
||||
mDeviceStateHandler.unregisterListener(mListener);
|
||||
mSoundTriggerHelper.detach();
|
||||
detachSessionLogger(mEventLogger);
|
||||
@@ -943,9 +997,8 @@ public class SoundTriggerService extends SystemService {
|
||||
}
|
||||
|
||||
private void enforceDetectionPermissions(ComponentName detectionService) {
|
||||
PackageManager packageManager = mContext.getPackageManager();
|
||||
String packageName = detectionService.getPackageName();
|
||||
if (packageManager.checkPermission(
|
||||
if (mPackageManager.checkPermission(
|
||||
Manifest.permission.CAPTURE_AUDIO_HOTWORD, packageName)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
throw new SecurityException(detectionService.getPackageName() + " does not have"
|
||||
@@ -1633,6 +1686,7 @@ public class SoundTriggerService extends SystemService {
|
||||
private final EventLogger mEventLogger;
|
||||
private final Identity mOriginatorIdentity;
|
||||
private final @NonNull DeviceStateListener mListener;
|
||||
private final MyAppOpsListener mAppOpsListener;
|
||||
|
||||
private final SparseArray<UUID> mModelUuid = new SparseArray<>(1);
|
||||
|
||||
@@ -1653,6 +1707,12 @@ public class SoundTriggerService extends SystemService {
|
||||
}
|
||||
mListener = (SoundTriggerDeviceState state)
|
||||
-> mSoundTriggerHelper.onDeviceStateChanged(state);
|
||||
mAppOpsListener = new MyAppOpsListener(mOriginatorIdentity,
|
||||
mSoundTriggerHelper::onAppOpStateChanged);
|
||||
mAppOpsListener.forceOpChangeRefresh();
|
||||
mAppOpsManager.startWatchingMode(AppOpsManager.OPSTR_RECORD_AUDIO,
|
||||
mOriginatorIdentity.packageName, AppOpsManager.WATCH_FOREGROUND_CHANGES,
|
||||
mAppOpsListener);
|
||||
mDeviceStateHandler.registerListener(mListener);
|
||||
}
|
||||
|
||||
@@ -1720,6 +1780,9 @@ public class SoundTriggerService extends SystemService {
|
||||
}
|
||||
|
||||
private void detachInternal() {
|
||||
if (mAppOpsListener != null) {
|
||||
mAppOpsManager.stopWatchingMode(mAppOpsListener);
|
||||
}
|
||||
mEventLogger.enqueue(new SessionEvent(Type.DETACH, null));
|
||||
detachSessionLogger(mEventLogger);
|
||||
mDeviceStateHandler.unregisterListener(mListener);
|
||||
|
||||
Reference in New Issue
Block a user