Merge "VibratorManagerService: Stop ongoing vibration when phone enters DND mode" into main
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index eda755c..b01ffe5 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -31,3 +31,14 @@
     description: "Enables the adaptive haptics feature"
     bug: "305961689"
 }
+
+flag {
+    namespace: "haptics"
+    name: "cancel_by_appops"
+    description: "Cancels ongoing vibrations when the appops mode changes to disallow them"
+    bug: "230745615"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto
index 1d9b0db..5a4d6db 100644
--- a/core/proto/android/server/vibrator/vibratormanagerservice.proto
+++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto
@@ -146,6 +146,7 @@
         IGNORED_FROM_VIRTUAL_DEVICE = 26;
         IGNORED_ON_WIRELESS_CHARGER = 27;
         IGNORED_MISSING_PERMISSION = 28;
+        CANCELLED_BY_APP_OPS = 29;
         reserved 17; // prev IGNORED_UNKNOWN_VIBRATION
     }
 }
diff --git a/services/core/java/com/android/server/vibrator/Vibration.java b/services/core/java/com/android/server/vibrator/Vibration.java
index 84c37180..6537228 100644
--- a/services/core/java/com/android/server/vibrator/Vibration.java
+++ b/services/core/java/com/android/server/vibrator/Vibration.java
@@ -69,6 +69,7 @@
         CANCELLED_BY_USER(VibrationProto.CANCELLED_BY_USER),
         CANCELLED_BY_UNKNOWN_REASON(VibrationProto.CANCELLED_BY_UNKNOWN_REASON),
         CANCELLED_SUPERSEDED(VibrationProto.CANCELLED_SUPERSEDED),
+        CANCELLED_BY_APP_OPS(VibrationProto.CANCELLED_BY_APP_OPS),
         IGNORED_ERROR_APP_OPS(VibrationProto.IGNORED_ERROR_APP_OPS),
         IGNORED_ERROR_CANCELLING(VibrationProto.IGNORED_ERROR_CANCELLING),
         IGNORED_ERROR_SCHEDULING(VibrationProto.IGNORED_ERROR_SCHEDULING),
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 3dcc7a6..7f60dc44 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -193,6 +193,27 @@
         }
     };
 
+    @VisibleForTesting
+    final AppOpsManager.OnOpChangedInternalListener mAppOpsChangeListener =
+            new AppOpsManager.OnOpChangedInternalListener() {
+                @Override
+                public void onOpChanged(int op, String packageName) {
+                    if (op != AppOpsManager.OP_VIBRATE) {
+                        return;
+                    }
+                    synchronized (mLock) {
+                        if (shouldCancelAppOpModeChangedLocked(mNextVibration)) {
+                            clearNextVibrationLocked(
+                                    new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_APP_OPS));
+                        }
+                        if (shouldCancelAppOpModeChangedLocked(mCurrentVibration)) {
+                            mCurrentVibration.notifyCancelled(new Vibration.EndInfo(
+                                    Vibration.Status.CANCELLED_BY_APP_OPS), /* immediate= */ false);
+                        }
+                    }
+                }
+            };
+
     static native long nativeInit(OnSyncedVibrationCompleteListener listener);
 
     static native long nativeGetFinalizer();
@@ -238,6 +259,9 @@
         mBatteryStatsService = injector.getBatteryStatsService();
 
         mAppOps = mContext.getSystemService(AppOpsManager.class);
+        if (Flags.cancelByAppops()) {
+            mAppOps.startWatchingMode(AppOpsManager.OP_VIBRATE, null, mAppOpsChangeListener);
+        }
 
         PowerManager pm = context.getSystemService(PowerManager.class);
         mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*vibrator*");
@@ -1390,6 +1414,15 @@
     }
 
     @GuardedBy("mLock")
+    private boolean shouldCancelAppOpModeChangedLocked(@Nullable VibrationStepConductor conductor) {
+        if (conductor == null) {
+            return false;
+        }
+        return checkAppOpModeLocked(conductor.getVibration().callerInfo)
+                != AppOpsManager.MODE_ALLOWED;
+    }
+
+    @GuardedBy("mLock")
     private void onAllVibratorsLocked(Consumer<VibratorController> consumer) {
         for (int i = 0; i < mVibrators.size(); i++) {
             consumer.accept(mVibrators.valueAt(i));
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index d6c0fef..1875284 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -781,6 +781,34 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_CANCEL_BY_APPOPS)
+    public void vibrate_thenDeniedAppOps_getsCancelled() throws Throwable {
+        mockVibrators(1);
+        VibratorManagerService service = createSystemReadyService();
+
+        var vib = vibrate(service,
+                VibrationEffect.createWaveform(new long[]{100, 100, 100, 100}, 0), RINGTONE_ATTRS);
+
+        assertTrue(waitUntil(s -> s.isVibrating(1), service, TEST_TIMEOUT_MILLIS));
+
+        when(mAppOpsManagerMock.checkAudioOpNoThrow(eq(AppOpsManager.OP_VIBRATE),
+                eq(AudioAttributes.USAGE_NOTIFICATION_RINGTONE), anyInt(), anyString()))
+                .thenReturn(AppOpsManager.MODE_IGNORED);
+
+        service.mAppOpsChangeListener.onOpChanged(AppOpsManager.OP_VIBRATE, null);
+
+        assertTrue(waitUntil(s -> vib.hasEnded(), service, TEST_TIMEOUT_MILLIS));
+
+        var statsInfoCaptor = ArgumentCaptor.forClass(VibrationStats.StatsInfo.class);
+        verify(mVibratorFrameworkStatsLoggerMock, timeout(TEST_TIMEOUT_MILLIS))
+                .writeVibrationReportedAsync(statsInfoCaptor.capture());
+
+        VibrationStats.StatsInfo touchMetrics = statsInfoCaptor.getAllValues().get(0);
+        assertEquals(Vibration.Status.CANCELLED_BY_APP_OPS.getProtoEnumValue(),
+                touchMetrics.status);
+    }
+
+    @Test
     public void vibrate_withVibrationAttributes_usesCorrespondingAudioUsageInAppOpsManager() {
         VibratorManagerService service = createSystemReadyService();