diff options
| -rw-r--r-- | core/res/res/values/config.xml | 3 | ||||
| -rw-r--r-- | core/res/res/values/symbols.xml | 1 | ||||
| -rw-r--r-- | media/java/android/media/Ringtone.java | 52 | ||||
| -rw-r--r-- | media/java/android/media/RingtoneManager.java | 7 | ||||
| -rw-r--r-- | media/java/android/media/Utils.java | 85 |
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; + } } |