summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/res/res/values/config.xml3
-rw-r--r--core/res/res/values/symbols.xml1
-rw-r--r--media/java/android/media/Ringtone.java52
-rw-r--r--media/java/android/media/RingtoneManager.java7
-rw-r--r--media/java/android/media/Utils.java85
5 files changed, 148 insertions, 0 deletions
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 4beeb176a77d..38aff7590a42 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4153,6 +4153,9 @@
<!-- Indicating if keyboard vibration settings supported or not. -->
<bool name="config_keyboardVibrationSettingsSupported">false</bool>
+ <!-- Indicating if ringtone vibration settings supported or not. -->
+ <bool name="config_ringtoneVibrationSettingsSupported">false</bool>
+
<!-- If the device should still vibrate even in low power mode, for certain priority vibrations
(e.g. accessibility, alarms). This is mainly for Wear devices that don't have speakers. -->
<bool name="config_allowPriorityVibrationsInLowPowerMode">false</bool>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 80ec67a1c56d..46938948b133 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2132,6 +2132,7 @@
<java-symbol type="dimen" name="config_hapticChannelMaxVibrationAmplitude" />
<java-symbol type="dimen" name="config_keyboardHapticFeedbackFixedAmplitude" />
<java-symbol type="bool" name="config_keyboardVibrationSettingsSupported" />
+ <java-symbol type="bool" name="config_ringtoneVibrationSettingsSupported" />
<java-symbol type="integer" name="config_vibrationWaveformRampStepDuration" />
<java-symbol type="bool" name="config_ignoreVibrationsOnWirelessCharger" />
<java-symbol type="integer" name="config_vibrationWaveformRampDownDuration" />
diff --git a/media/java/android/media/Ringtone.java b/media/java/android/media/Ringtone.java
index e78dc31646ca..b448ecd00098 100644
--- a/media/java/android/media/Ringtone.java
+++ b/media/java/android/media/Ringtone.java
@@ -16,6 +16,8 @@
package android.media;
+import static android.media.Utils.parseVibrationEffect;
+
import android.annotation.Nullable;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentProvider;
@@ -24,17 +26,23 @@ import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.content.res.Resources.NotFoundException;
import android.database.Cursor;
+import android.media.audio.Flags;
import android.media.audiofx.HapticGenerator;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.RemoteException;
import android.os.Trace;
+import android.os.VibrationAttributes;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
import android.provider.MediaStore;
import android.provider.MediaStore.MediaColumns;
import android.provider.Settings;
import android.util.Log;
+
import com.android.internal.annotations.VisibleForTesting;
+
import java.io.IOException;
import java.util.ArrayList;
@@ -62,6 +70,11 @@ public class Ringtone {
// keep references on active Ringtones until stopped or completion listener called.
private static final ArrayList<Ringtone> sActiveRingtones = new ArrayList<Ringtone>();
+ private static final VibrationAttributes VIBRATION_ATTRIBUTES =
+ new VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_RINGTONE).build();
+
+ private static final int VIBRATION_LOOP_DELAY_MS = 200;
+
private final Context mContext;
private final AudioManager mAudioManager;
private VolumeShaper.Configuration mVolumeShaperConfig;
@@ -95,6 +108,10 @@ public class Ringtone {
private float mVolume = 1.0f;
private boolean mHapticGeneratorEnabled = false;
private final Object mPlaybackSettingsLock = new Object();
+ private final Vibrator mVibrator;
+ private final boolean mRingtoneVibrationSupported;
+ private VibrationEffect mVibrationEffect;
+ private boolean mIsVibrating;
/** {@hide} */
@UnsupportedAppUsage
@@ -104,6 +121,8 @@ public class Ringtone {
mAllowRemote = allowRemote;
mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null;
mRemoteToken = allowRemote ? new Binder() : null;
+ mVibrator = mContext.getSystemService(Vibrator.class);
+ mRingtoneVibrationSupported = Utils.isRingtoneVibrationSettingsSupported(mContext);
}
/**
@@ -487,6 +506,23 @@ public class Ringtone {
if (mUri == null) {
destroyLocalPlayer();
}
+ if (Flags.enableRingtoneHapticsCustomization()
+ && mRingtoneVibrationSupported && mUri != null) {
+ mVibrationEffect = parseVibrationEffect(mVibrator, Utils.getVibrationUri(mUri));
+ if (mVibrationEffect != null) {
+ mVibrationEffect =
+ mVibrationEffect.applyRepeatingIndefinitely(true, VIBRATION_LOOP_DELAY_MS);
+ }
+ }
+ }
+
+ /**
+ * Returns the {@link VibrationEffect} has been created for this ringtone.
+ * @hide
+ */
+ @VisibleForTesting
+ public VibrationEffect getVibrationEffect() {
+ return mVibrationEffect;
}
/** {@hide} */
@@ -530,6 +566,17 @@ public class Ringtone {
Log.w(TAG, "Neither local nor remote playback available");
}
}
+ if (Flags.enableRingtoneHapticsCustomization() && mRingtoneVibrationSupported) {
+ playVibration();
+ }
+ }
+
+ private void playVibration() {
+ if (mVibrationEffect == null) {
+ return;
+ }
+ mIsVibrating = true;
+ mVibrator.vibrate(mVibrationEffect, VIBRATION_ATTRIBUTES);
}
/**
@@ -545,6 +592,11 @@ public class Ringtone {
Log.w(TAG, "Problem stopping ringtone: " + e);
}
}
+ if (Flags.enableRingtoneHapticsCustomization()
+ && mRingtoneVibrationSupported && mIsVibrating) {
+ mVibrator.cancel();
+ mIsVibrating = false;
+ }
}
private void destroyLocalPlayer() {
diff --git a/media/java/android/media/RingtoneManager.java b/media/java/android/media/RingtoneManager.java
index 47e3a0fff076..f0ab6ecc5cda 100644
--- a/media/java/android/media/RingtoneManager.java
+++ b/media/java/android/media/RingtoneManager.java
@@ -34,6 +34,7 @@ import android.content.pm.UserInfo;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.StaleDataException;
+import android.media.audio.Flags;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
@@ -809,6 +810,12 @@ public class RingtoneManager {
// Don't set the stream type
Ringtone ringtone = getRingtone(context, ringtoneUri, -1 /* streamType */,
volumeShaperConfig, false);
+ if (Flags.enableRingtoneHapticsCustomization()
+ && Utils.isRingtoneVibrationSettingsSupported(context)
+ && Utils.hasVibration(ringtoneUri) && hasHapticChannels(ringtoneUri)) {
+ audioAttributes = new AudioAttributes.Builder(
+ audioAttributes).setHapticChannelsMuted(true).build();
+ }
if (ringtone != null) {
ringtone.setAudioAttributesField(audioAttributes);
if (!ringtone.createLocalMediaPlayer()) {
diff --git a/media/java/android/media/Utils.java b/media/java/android/media/Utils.java
index d07f6118f6f4..41e9b65da93a 100644
--- a/media/java/android/media/Utils.java
+++ b/media/java/android/media/Utils.java
@@ -20,12 +20,17 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Binder;
import android.os.Environment;
import android.os.FileUtils;
import android.os.Handler;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.os.vibrator.persistence.ParsedVibration;
+import android.os.vibrator.persistence.VibrationXmlParser;
import android.provider.OpenableColumns;
import android.util.Log;
import android.util.Pair;
@@ -36,7 +41,11 @@ import android.util.Size;
import com.android.internal.annotations.GuardedBy;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
@@ -55,6 +64,8 @@ import java.util.concurrent.Executor;
public class Utils {
private static final String TAG = "Utils";
+ public static final String VIBRATION_URI_PARAM = "vibration_uri";
+
/**
* Sorts distinct (non-intersecting) range array in ascending order.
* @throws java.lang.IllegalArgumentException if ranges are not distinct
@@ -688,4 +699,78 @@ public class Utils {
}
return anonymizeBluetoothAddress(address);
}
+
+ /**
+ * Whether the device supports ringtone vibration settings.
+ *
+ * @param context the {@link Context}
+ * @return {@code true} if the device supports ringtone vibration
+ */
+ public static boolean isRingtoneVibrationSettingsSupported(Context context) {
+ final Resources res = context.getResources();
+ return res != null && res.getBoolean(
+ com.android.internal.R.bool.config_ringtoneVibrationSettingsSupported);
+ }
+
+ /**
+ * Whether the given ringtone Uri has vibration Uri parameter
+ *
+ * @param ringtoneUri the ringtone Uri
+ * @return {@code true} if the Uri has vibration parameter
+ */
+ public static boolean hasVibration(Uri ringtoneUri) {
+ final String vibrationUriString = ringtoneUri.getQueryParameter(VIBRATION_URI_PARAM);
+ return vibrationUriString != null;
+ }
+
+ /**
+ * Gets the vibration Uri from given ringtone Uri
+ *
+ * @param ringtoneUri the ringtone Uri
+ * @return parsed {@link Uri} of vibration parameter, {@code null} if the vibration parameter
+ * is not found.
+ */
+ public static Uri getVibrationUri(Uri ringtoneUri) {
+ final String vibrationUriString = ringtoneUri.getQueryParameter(VIBRATION_URI_PARAM);
+ if (vibrationUriString == null) {
+ return null;
+ }
+ return Uri.parse(vibrationUriString);
+ }
+
+ /**
+ * Returns the parsed {@link VibrationEffect} from given vibration Uri.
+ *
+ * @param vibrator the vibrator to resolve the vibration file
+ * @param vibrationUri the vibration file Uri to represent a vibration
+ */
+ @SuppressWarnings("FlaggedApi") // VibrationXmlParser is available internally as hidden APIs.
+ public static VibrationEffect parseVibrationEffect(Vibrator vibrator, Uri vibrationUri) {
+ if (vibrationUri == null) {
+ Log.w(TAG, "The vibration Uri is null.");
+ return null;
+ }
+ String filePath = vibrationUri.getPath();
+ if (filePath == null) {
+ Log.w(TAG, "The file path is null.");
+ return null;
+ }
+ File vibrationFile = new File(filePath);
+ if (vibrationFile.exists() && vibrationFile.canRead()) {
+ try {
+ FileInputStream fileInputStream = new FileInputStream(vibrationFile);
+ ParsedVibration parsedVibration =
+ VibrationXmlParser.parseDocument(
+ new InputStreamReader(fileInputStream, StandardCharsets.UTF_8));
+ return parsedVibration.resolve(vibrator);
+ } catch (IOException e) {
+ Log.e(TAG, "FileNotFoundException" + e);
+ }
+ } else {
+ // File not found or cannot be read
+ Log.w(TAG, "File exists:" + vibrationFile.exists()
+ + ", canRead:" + vibrationFile.canRead());
+ }
+ return null;
+ }
}