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

Commit 43193539 authored by Wilson Wu's avatar Wilson Wu Committed by Android (Google) Code Review
Browse files

Merge "Support custom vibration play for ringtone" into main

parents c6eb2c6d 15a6444b
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;
    }
}