Merge "Enable the customised vibration of (1) default notification (2) channel notification." into main
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index e541246..b9f0968 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -330,10 +330,19 @@
         }
 
         final long[] vibrationPattern = channel.getVibrationPattern();
-        if (vibrationPattern == null) {
-            return helper.createDefaultVibration(insistent);
+        if (vibrationPattern != null) {
+            return helper.createWaveformVibration(vibrationPattern, insistent);
         }
-        return helper.createWaveformVibration(vibrationPattern, insistent);
+
+        if (com.android.server.notification.Flags.notificationVibrationInSoundUri()) {
+            final VibrationEffect vibrationEffectFromSoundUri =
+                    helper.createVibrationEffectFromSoundUri(channel.getSound());
+            if (vibrationEffectFromSoundUri != null) {
+                return vibrationEffectFromSoundUri;
+            }
+        }
+
+        return helper.createDefaultVibration(insistent);
     }
 
     private VibrationEffect calculateVibration() {
diff --git a/services/core/java/com/android/server/notification/VibratorHelper.java b/services/core/java/com/android/server/notification/VibratorHelper.java
index 8a0e595..fbe7772 100644
--- a/services/core/java/com/android/server/notification/VibratorHelper.java
+++ b/services/core/java/com/android/server/notification/VibratorHelper.java
@@ -24,6 +24,9 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.media.AudioAttributes;
+import android.media.RingtoneManager;
+import android.media.Utils;
+import android.net.Uri;
 import android.os.Process;
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
@@ -51,6 +54,7 @@
     @Nullable private final float[] mDefaultPwlePattern;
     @Nullable private final float[] mFallbackPwlePattern;
     private final int mDefaultVibrationAmplitude;
+    private final Context mContext;
 
     public VibratorHelper(Context context) {
         mVibrator = context.getSystemService(Vibrator.class);
@@ -68,6 +72,7 @@
                 com.android.internal.R.array.config_notificationFallbackVibeWaveform);
         mDefaultVibrationAmplitude = context.getResources().getInteger(
             com.android.internal.R.integer.config_defaultVibrationAmplitude);
+        mContext = context;
     }
 
     /**
@@ -184,6 +189,16 @@
      * @param insistent {@code true} if the vibration should loop until it is cancelled.
      */
     public VibrationEffect createDefaultVibration(boolean insistent) {
+        if (com.android.server.notification.Flags.notificationVibrationInSoundUri()) {
+            final Uri defaultRingtoneUri = RingtoneManager.getActualDefaultRingtoneUri(mContext,
+                    RingtoneManager.TYPE_NOTIFICATION);
+            final VibrationEffect vibrationEffectFromSoundUri =
+                    createVibrationEffectFromSoundUri(defaultRingtoneUri);
+            if (vibrationEffectFromSoundUri != null) {
+                return vibrationEffectFromSoundUri;
+            }
+        }
+
         if (mVibrator.hasFrequencyControl()) {
             VibrationEffect effect = createPwleWaveformVibration(mDefaultPwlePattern, insistent);
             if (effect != null) {
@@ -193,6 +208,22 @@
         return createWaveformVibration(mDefaultPattern, insistent);
     }
 
+    /**
+     * Safely create a {@link VibrationEffect} from given an uri {@code Uri}.
+     * with query parameter "vibration_uri"
+     *
+     * Use this function if the {@code Uri} is with a query parameter "vibration_uri" and the
+     * vibration_uri represents a valid vibration effect in xml
+     *
+     * @param uri {@code Uri} an uri including query parameter "vibraiton_uri"
+     */
+    public @Nullable VibrationEffect createVibrationEffectFromSoundUri(Uri uri) {
+        if (uri == null) {
+            return null;
+        }
+        return Utils.parseVibrationEffect(mVibrator, Utils.getVibrationUri(uri));
+    }
+
     /** Returns if a given vibration can be played by the vibrator that does notification buzz. */
     public boolean areEffectComponentsSupported(VibrationEffect effect) {
         return mVibrator.areVibrationFeaturesSupported(effect);
diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig
index aac2c40..be3adc1 100644
--- a/services/core/java/com/android/server/notification/flags.aconfig
+++ b/services/core/java/com/android/server/notification/flags.aconfig
@@ -156,3 +156,10 @@
   description: "This flag enables forced auto-grouping conversations"
   bug: "336488844"
 }
+
+flag {
+  name: "notification_vibration_in_sound_uri"
+  namespace: "systemui"
+  description: "This flag enables sound uri with vibration source"
+  bug: "358524009"
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
index f572e7a..50a5f65 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
@@ -62,6 +62,8 @@
 import android.graphics.Color;
 import android.graphics.drawable.Icon;
 import android.media.AudioAttributes;
+import android.media.RingtoneManager;
+import android.media.Utils;
 import android.metrics.LogMaker;
 import android.net.Uri;
 import android.os.Build;
@@ -69,6 +71,7 @@
 import android.os.UserHandle;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
+import android.os.VibratorInfo;
 import android.platform.test.annotations.EnableFlags;
 import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
@@ -93,6 +96,9 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
 import java.util.ArrayList;
 
 @SmallTest
@@ -141,6 +147,7 @@
 
         when(mMockContext.getSystemService(eq(Vibrator.class))).thenReturn(mVibrator);
         when(mVibrator.areVibrationFeaturesSupported(any())).thenReturn(true);
+        when(mVibrator.getInfo()).thenReturn(VibratorInfo.EMPTY_VIBRATOR_INFO);
         final Resources res = mContext.getResources();
         when(mMockContext.getResources()).thenReturn(res);
         when(mMockContext.getPackageManager()).thenReturn(mPm);
@@ -511,6 +518,51 @@
     }
 
     @Test
+    @EnableFlags(com.android.server.notification.Flags.FLAG_NOTIFICATION_VIBRATION_IN_SOUND_URI)
+    public void testVibration_customVibrationForSound_withoutVibrationUri() {
+        // prepare testing data
+        Uri backupDefaultUri = RingtoneManager.getActualDefaultRingtoneUri(mMockContext,
+                RingtoneManager.TYPE_NOTIFICATION);
+        RingtoneManager.setActualDefaultRingtoneUri(mMockContext, RingtoneManager.TYPE_NOTIFICATION,
+                Settings.System.DEFAULT_NOTIFICATION_URI);
+        defaultChannel.enableVibration(true);
+        defaultChannel.setSound(Settings.System.DEFAULT_NOTIFICATION_URI, CUSTOM_ATTRIBUTES);
+        StatusBarNotification sbn = getNotification(
+                /* channelVibrationPattern= */ null,
+                /* channelVibrationEffect= */ null,
+                /* insistent= */ false);
+        NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel);
+
+        try {
+            assertEquals(
+                    new VibratorHelper(mMockContext).createDefaultVibration(false),
+                    record.getVibration());
+        } finally {
+            // restore the data
+            RingtoneManager.setActualDefaultRingtoneUri(mMockContext,
+                    RingtoneManager.TYPE_NOTIFICATION,
+                    backupDefaultUri);
+        }
+    }
+
+    @Test
+    @EnableFlags(com.android.server.notification.Flags.FLAG_NOTIFICATION_VIBRATION_IN_SOUND_URI)
+    public void testVibration_customVibrationForSound_withVibrationUri() throws IOException {
+        defaultChannel.enableVibration(true);
+        VibrationInfo vibration = getTestingVibration(mVibrator);
+        Uri uriWithVibration = getVibrationUriAppended(
+                Settings.System.DEFAULT_NOTIFICATION_URI, vibration.mUri);
+        defaultChannel.setSound(uriWithVibration, CUSTOM_ATTRIBUTES);
+        StatusBarNotification sbn = getNotification(
+                /* channelVibrationPattern= */ null,
+                /* channelVibrationEffect= */ null,
+                /* insistent= */ false);
+        NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel);
+
+        assertEquals(vibration.mVibrationEffect, record.getVibration());
+    }
+
+    @Test
     public void testImportance_preUpgrade() {
         StatusBarNotification sbn = getNotification(PKG_N_MR1, true /* noisy */,
                 true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
@@ -1594,4 +1646,39 @@
 
         assertThat(record.getAudioAttributes().getUsage()).isEqualTo(USAGE_ALARM);
     }
+
+    static class VibrationInfo {
+        public VibrationEffect mVibrationEffect;
+        public Uri mUri;
+        VibrationInfo(VibrationEffect vibrationEffect, Uri uri) {
+            mVibrationEffect = vibrationEffect;
+            mUri = uri;
+        }
+    }
+
+    private static VibrationInfo getTestingVibration(Vibrator vibrator) throws IOException {
+        File tempVibrationFile = File.createTempFile("test_vibration_file", ".xml");
+        FileWriter writer = new FileWriter(tempVibrationFile);
+        writer.write("<vibration-effect>\n"
+                + "    <waveform-effect>\n"
+                + "        <!-- PRIMING -->\n"
+                + "        <waveform-entry durationMs=\"0\" amplitude=\"0\"/>\n"
+                + "        <waveform-entry durationMs=\"12\" amplitude=\"255\"/>\n"
+                + "        <waveform-entry durationMs=\"250\" amplitude=\"0\"/>\n"
+                + "        <waveform-entry durationMs=\"12\" amplitude=\"255\"/>\n"
+                + "        <waveform-entry durationMs=\"500\" amplitude=\"0\"/>\n"
+                + "    </waveform-effect>\n"
+                + "</vibration-effect>"); // Your test XML content
+        writer.close();
+        Uri vibrationUri = Uri.parse(tempVibrationFile.toURI().toString());
+
+        VibrationEffect vibrationEffect = Utils.parseVibrationEffect(vibrator, vibrationUri);
+        return new VibrationInfo(vibrationEffect, vibrationUri);
+    }
+
+    private static Uri getVibrationUriAppended(Uri audioUri, Uri vibrationUri) {
+        Uri.Builder builder = audioUri.buildUpon();
+        builder.appendQueryParameter(Utils.VIBRATION_URI_PARAM, vibrationUri.toString());
+        return builder.build();
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java
index 0993bec..4d2396c 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/VibratorHelperTest.java
@@ -22,8 +22,12 @@
 
 import static org.mockito.Mockito.when;
 
+import android.media.Utils;
+import android.net.Uri;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
+import android.os.VibratorInfo;
+import android.provider.Settings;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -36,6 +40,10 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class VibratorHelperTest extends UiServiceTestCase {
@@ -86,7 +94,34 @@
     }
 
     @Test
+    public void createVibrationEffectFromSoundUri_nullInput() {
+        assertNull(mVibratorHelper.createVibrationEffectFromSoundUri(null));
+    }
+
+    @Test
+    public void createVibrationEffectFromSoundUri_emptyUri() {
+        assertNull(mVibratorHelper.createVibrationEffectFromSoundUri(Uri.EMPTY));
+    }
+
+    @Test
+    public void createVibrationEffectFromSoundUri_uriWithoutRequiredQueryParameter() {
+        Uri uri = Settings.System.DEFAULT_NOTIFICATION_URI;
+        assertNull(mVibratorHelper.createVibrationEffectFromSoundUri(uri));
+    }
+
+    @Test
+    public void createVibrationEffectFromSoundUri_uriWithVibrationUri() throws IOException {
+        // prepare the uri with vibration
+        when(mVibrator.getInfo()).thenReturn(VibratorInfo.EMPTY_VIBRATOR_INFO);
+        Uri validUri = getVibrationUriAppended(Settings.System.DEFAULT_NOTIFICATION_URI);
+
+        assertSingleVibration(mVibratorHelper.createVibrationEffectFromSoundUri(validUri));
+    }
+
+    @Test
     public void createVibration_insistent_createsRepeatingVibration() {
+        when(mVibrator.getInfo()).thenReturn(VibratorInfo.EMPTY_VIBRATOR_INFO);
+
         when(mVibrator.hasFrequencyControl()).thenReturn(false);
         assertRepeatingVibration(mVibratorHelper.createDefaultVibration(/* insistent= */ true));
         assertRepeatingVibration(mVibratorHelper.createFallbackVibration(/* insistent= */ true));
@@ -98,6 +133,8 @@
 
     @Test
     public void createVibration_nonInsistent_createsSingleShotVibration() {
+        when(mVibrator.getInfo()).thenReturn(VibratorInfo.EMPTY_VIBRATOR_INFO);
+
         when(mVibrator.hasFrequencyControl()).thenReturn(false);
         assertSingleVibration(mVibratorHelper.createDefaultVibration(/* insistent= */ false));
         assertSingleVibration(mVibratorHelper.createFallbackVibration(/* insistent= */ false));
@@ -120,4 +157,26 @@
                 effect instanceof VibrationEffect.Composed);
         return ((VibrationEffect.Composed) effect).getRepeatIndex();
     }
+
+    private static Uri getVibrationUriAppended(Uri baseUri) throws IOException {
+        File tempVibrationFile = File.createTempFile("test_vibration_file", ".xml");
+        FileWriter writer = new FileWriter(tempVibrationFile);
+        writer.write("<vibration-effect>\n"
+                + "    <waveform-effect>\n"
+                + "        <!-- PRIMING -->\n"
+                + "        <waveform-entry durationMs=\"0\" amplitude=\"0\"/>\n"
+                + "        <waveform-entry durationMs=\"12\" amplitude=\"255\"/>\n"
+                + "        <waveform-entry durationMs=\"250\" amplitude=\"0\"/>\n"
+                + "        <waveform-entry durationMs=\"12\" amplitude=\"255\"/>\n"
+                + "        <waveform-entry durationMs=\"500\" amplitude=\"0\"/>\n"
+                + "    </waveform-effect>\n"
+                + "</vibration-effect>"); // Your test XML content
+        writer.close();
+
+        Uri.Builder builder = baseUri.buildUpon();
+        builder.appendQueryParameter(
+                Utils.VIBRATION_URI_PARAM,
+                tempVibrationFile.toURI().toString());
+        return builder.build();
+    }
 }