RESTRICT AUTOMERGE Restart recognition when failing to deliver event

If the client does not know that recognition is not running due to a
recognition event not having been delivered, we should attempt to
restart it.

Fixes: 193579626
Test: Manual verification of the scenario described in the bug.


Change-Id: I4fc3b3e8defed59a900fd156273e9e695a322b0c
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java
index c1f8240..9999aff 100644
--- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewarePermission.java
@@ -133,13 +133,8 @@
      * Throws a {@link SecurityException} iff the originator has permission to receive data.
      */
     void enforcePermissionsForDataDelivery(@NonNull Identity identity, @NonNull String reason) {
-        // TODO(b/186164881): remove
-        // START TEMP HACK
-        enforcePermissionForPreflight(mContext, identity, RECORD_AUDIO);
-        int hotwordOp = AppOpsManager.strOpToOp(AppOpsManager.OPSTR_RECORD_AUDIO_HOTWORD);
-        mContext.getSystemService(AppOpsManager.class).noteOpNoThrow(hotwordOp, identity.uid,
-                identity.packageName, identity.attributionTag, reason);
-        // END TEMP HACK
+        enforcePermissionForDataDelivery(mContext, identity, RECORD_AUDIO,
+                reason);
         enforcePermissionForDataDelivery(mContext, identity, CAPTURE_AUDIO_HOTWORD,
                 reason);
     }
@@ -167,8 +162,8 @@
 
     /**
      * Throws a {@link SecurityException} if originator permanently doesn't have the given
-     * permission, or a {@link ServiceSpecificException} with a {@link
-     * Status#TEMPORARY_PERMISSION_DENIED} if caller originator doesn't have the given permission.
+     * permission.
+     * Soft (temporary) denials are considered OK for preflight purposes.
      *
      * @param context    A {@link Context}, used for permission checks.
      * @param identity   The identity to check.
@@ -180,15 +175,12 @@
                 permission);
         switch (status) {
             case PermissionChecker.PERMISSION_GRANTED:
+            case PermissionChecker.PERMISSION_SOFT_DENIED:
                 return;
             case PermissionChecker.PERMISSION_HARD_DENIED:
                 throw new SecurityException(
                         String.format("Failed to obtain permission %s for identity %s", permission,
                                 ObjectPrinter.print(identity, true, 16)));
-            case PermissionChecker.PERMISSION_SOFT_DENIED:
-                throw new ServiceSpecificException(Status.TEMPORARY_PERMISSION_DENIED,
-                        String.format("Failed to obtain permission %s for identity %s", permission,
-                                ObjectPrinter.print(identity, true, 16)));
             default:
                 throw new RuntimeException("Unexpected perimission check result.");
         }
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java
index 95a30c7..458eae9 100644
--- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareValidation.java
@@ -318,6 +318,8 @@
          */
         private Map<Integer, ModelParameterRange> parameterSupport = new HashMap<>();
 
+        private RecognitionConfig mConfig;
+
         /**
          * Check that the given parameter is known to be supported for this model.
          *
@@ -369,6 +371,14 @@
         void setActivityState(Activity activity) {
             mActivityState.set(activity.ordinal());
         }
+
+        void setRecognitionConfig(@NonNull RecognitionConfig config) {
+            mConfig = config;
+        }
+
+        RecognitionConfig getRecognitionConfig() {
+            return mConfig;
+        }
     }
 
     /**
@@ -502,6 +512,7 @@
                     // Normally, we would set the state after the operation succeeds. However, since
                     // the activity state may be reset outside of the lock, we set it here first,
                     // and reset it in case of exception.
+                    modelState.setRecognitionConfig(config);
                     modelState.setActivityState(ModelState.Activity.ACTIVE);
                     mDelegate.startRecognition(modelHandle, config);
                 } catch (Exception e) {
@@ -542,6 +553,27 @@
             }
         }
 
+        private void restartIfIntercepted(int modelHandle) {
+            synchronized (SoundTriggerMiddlewareValidation.this) {
+                // State validation.
+                if (mState == ModuleStatus.DETACHED) {
+                    return;
+                }
+                ModelState modelState = mLoadedModels.get(modelHandle);
+                if (modelState == null
+                        || modelState.getActivityState() != ModelState.Activity.INTERCEPTED) {
+                    return;
+                }
+                try {
+                    mDelegate.startRecognition(modelHandle, modelState.getRecognitionConfig());
+                    modelState.setActivityState(ModelState.Activity.ACTIVE);
+                    Log.i(TAG, "Restarted intercepted model " + modelHandle);
+                } catch (Exception e) {
+                    Log.i(TAG, "Failed to restart intercepted model " + modelHandle, e);
+                }
+            }
+        }
+
         @Override
         public void forceRecognitionEvent(int modelHandle) {
             // Input validation (always valid).
@@ -753,6 +785,10 @@
                     Log.e(TAG, "Client callback exception.", e);
                     if (event.status != RecognitionStatus.FORCED) {
                         modelState.setActivityState(ModelState.Activity.INTERCEPTED);
+                        // If we failed to deliver an actual event to the client, they would never
+                        // know to restart it whenever circumstances change. Thus, we restart it
+                        // here. We do this from a separate thread to avoid any race conditions.
+                        new Thread(() -> restartIfIntercepted(modelHandle)).start();
                     }
                 }
             }
@@ -780,6 +816,10 @@
                     Log.e(TAG, "Client callback exception.", e);
                     if (event.common.status != RecognitionStatus.FORCED) {
                         modelState.setActivityState(ModelState.Activity.INTERCEPTED);
+                        // If we failed to deliver an actual event to the client, they would never
+                        // know to restart it whenever circumstances change. Thus, we restart it
+                        // here. We do this from a separate thread to avoid any race conditions.
+                        new Thread(() -> restartIfIntercepted(modelHandle)).start();
                     }
                 }
             }