Loading core/res/res/values/config.xml +3 −0 Original line number Diff line number Diff line Loading @@ -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> Loading core/res/res/values/symbols.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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" /> Loading media/java/android/media/Ringtone.java +52 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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 Loading @@ -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); } /** Loading Loading @@ -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} */ Loading Loading @@ -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); } /** Loading @@ -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() { Loading media/java/android/media/RingtoneManager.java +7 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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()) { Loading media/java/android/media/Utils.java +85 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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 Loading Loading @@ -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; } } Loading
core/res/res/values/config.xml +3 −0 Original line number Diff line number Diff line Loading @@ -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> Loading
core/res/res/values/symbols.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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" /> Loading
media/java/android/media/Ringtone.java +52 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading Loading @@ -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; Loading Loading @@ -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 Loading @@ -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); } /** Loading Loading @@ -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} */ Loading Loading @@ -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); } /** Loading @@ -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() { Loading
media/java/android/media/RingtoneManager.java +7 −0 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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()) { Loading
media/java/android/media/Utils.java +85 −0 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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 Loading Loading @@ -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; } }