Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 15a6444b authored by Wilson Wu's avatar Wilson Wu
Browse files

Support custom vibration play for ringtone

With this CL, Ringtone is able to support the
custom vibration playback for the vibration_uri.

Also introduce a config to indicate if the device
support custom vibration or not, we can ensure it's
only enabled on certain devices.

Flag: android.media.audio.enable_ringtone_haptics_customization
Bug: 351974934
Bug: 351975294
Test: atest RingtoneManagerTest
Test: atest RingtoneTest
Change-Id: I8fc4f49e3801caec1dc2b0b611dd5b3854c6395f
parent 789e5eac
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -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>
+1 −0
Original line number Diff line number Diff line
@@ -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" />
+52 −0
Original line number Diff line number Diff line
@@ -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() {
+7 −0
Original line number Diff line number Diff line
@@ -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()) {
+85 −0
Original line number Diff line number Diff line
@@ -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;
    }
}