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

Commit cb110af3 authored by Simon Bowden's avatar Simon Bowden
Browse files

Factor out ringtone local vs remote playback into separate implementations.

This way, the ringtone class is more clearly interacting with exactly
one of them, and has more obvious semantics for (re)initialization.

Bug: 261571543
Test: manual
Change-Id: I6530781028af0ba46acb6c37f95d8b9d8ef4e4a4
parent 0e620799
Loading
Loading
Loading
Loading
+5 −3
Original line number Diff line number Diff line
@@ -30,11 +30,13 @@ interface IRingtonePlayer {
    @UnsupportedAppUsage
    oneway void play(IBinder token, in Uri uri, in AudioAttributes aa, float volume, boolean looping);
    oneway void playWithVolumeShaping(IBinder token, in Uri uri, in AudioAttributes aa,
        float volume, boolean looping, in @nullable VolumeShaper.Configuration volumeShaperConfig);
        float volume, boolean looping, boolean hapticGeneratorEnabled,
        in @nullable VolumeShaper.Configuration volumeShaperConfig);
    oneway void stop(IBinder token);
    boolean isPlaying(IBinder token);
    oneway void setPlaybackProperties(IBinder token, float volume, boolean looping,
        boolean hapticGeneratorEnabled);
    oneway void setLooping(IBinder token, boolean looping);
    oneway void setVolume(IBinder token, float volume);
    oneway void setHapticGeneratorEnabled(IBinder token, boolean hapticGeneratorEnabled);

    /** Used for Notification sound playback. */
    oneway void playAsync(in Uri uri, in UserHandle user, boolean looping, in AudioAttributes aa);
+244 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.media;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.audiofx.HapticGenerator;
import android.net.Uri;
import android.os.Trace;
import android.util.Log;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Objects;

/**
 * Plays a ringtone on the local process.
 * @hide
 */
public class LocalRingtonePlayer
        implements Ringtone.RingtonePlayer, MediaPlayer.OnCompletionListener {
    private static final String TAG = "LocalRingtonePlayer";

    // keep references on active Ringtones until stopped or completion listener called.
    private static final ArrayList<LocalRingtonePlayer> sActiveRingtones = new ArrayList<>();

    private final MediaPlayer mMediaPlayer;
    private final AudioAttributes mAudioAttributes;
    private final AudioManager mAudioManager;
    private final VolumeShaper mVolumeShaper;
    private HapticGenerator mHapticGenerator;

    private LocalRingtonePlayer(@NonNull MediaPlayer mediaPlayer,
            @NonNull AudioAttributes audioAttributes, @NonNull AudioManager audioManager,
            @Nullable HapticGenerator hapticGenerator, @Nullable VolumeShaper volumeShaper) {
        Objects.requireNonNull(mediaPlayer);
        Objects.requireNonNull(audioAttributes);
        Objects.requireNonNull(audioManager);
        mMediaPlayer = mediaPlayer;
        mAudioAttributes = audioAttributes;
        mAudioManager = audioManager;
        mVolumeShaper = volumeShaper;
        mHapticGenerator = hapticGenerator;
    }

    /**
     * Creates a {@link LocalRingtonePlayer} for a Uri, returning null if the Uri can't be
     * loaded in the local player.
     */
    @Nullable
    static LocalRingtonePlayer create(@NonNull Context context,
            @NonNull AudioManager audioManager, @NonNull Uri soundUri,
            @NonNull AudioAttributes audioAttributes,
            @Nullable VolumeShaper.Configuration volumeShaperConfig,
            @Nullable AudioDeviceInfo preferredDevice, boolean initialHapticGeneratorEnabled,
            boolean initialLooping, float initialVolume) {
        Objects.requireNonNull(context);
        Objects.requireNonNull(soundUri);
        Objects.requireNonNull(audioAttributes);
        Trace.beginSection("createLocalMediaPlayer");
        MediaPlayer mediaPlayer = new MediaPlayer();
        HapticGenerator hapticGenerator = null;
        try {
            mediaPlayer.setDataSource(context, soundUri);
            mediaPlayer.setAudioAttributes(audioAttributes);
            mediaPlayer.setPreferredDevice(preferredDevice);
            mediaPlayer.setLooping(initialLooping);
            mediaPlayer.setVolume(initialVolume);
            if (initialHapticGeneratorEnabled) {
                hapticGenerator = HapticGenerator.create(mediaPlayer.getAudioSessionId());
                hapticGenerator.setEnabled(true);
            }
            VolumeShaper volumeShaper = null;
            if (volumeShaperConfig != null) {
                volumeShaper = mediaPlayer.createVolumeShaper(volumeShaperConfig);
            }
            mediaPlayer.prepare();
            return new LocalRingtonePlayer(mediaPlayer, audioAttributes, audioManager,
                    hapticGenerator, volumeShaper);
        } catch (SecurityException | IOException e) {
            if (hapticGenerator != null) {
                hapticGenerator.release();
            }
            // volume shaper closes with media player
            mediaPlayer.release();
            return null;
        } finally {
            Trace.endSection();
        }
    }

    /**
     * Creates a {@link LocalRingtonePlayer} for an externally referenced file descriptor. This is
     * intended for loading a fallback from an internal resource, rather than via a Uri.
     */
    @Nullable
    static LocalRingtonePlayer createForFallback(
            @NonNull AudioManager audioManager, @NonNull AssetFileDescriptor afd,
            @NonNull AudioAttributes audioAttributes,
            @Nullable VolumeShaper.Configuration volumeShaperConfig,
            @Nullable AudioDeviceInfo preferredDevice,
            boolean initialLooping, float initialVolume) {
        // Haptic generator not supported for fallback.
        Objects.requireNonNull(audioManager);
        Objects.requireNonNull(afd);
        Objects.requireNonNull(audioAttributes);
        Trace.beginSection("createFallbackLocalMediaPlayer");

        MediaPlayer mediaPlayer = new MediaPlayer();
        try {
            if (afd.getDeclaredLength() < 0) {
                mediaPlayer.setDataSource(afd.getFileDescriptor());
            } else {
                mediaPlayer.setDataSource(afd.getFileDescriptor(),
                        afd.getStartOffset(),
                        afd.getDeclaredLength());
            }
            mediaPlayer.setAudioAttributes(audioAttributes);
            mediaPlayer.setPreferredDevice(preferredDevice);
            mediaPlayer.setLooping(initialLooping);
            mediaPlayer.setVolume(initialVolume);
            VolumeShaper volumeShaper = null;
            if (volumeShaperConfig != null) {
                volumeShaper = mediaPlayer.createVolumeShaper(volumeShaperConfig);
            }
            mediaPlayer.prepare();
            return new LocalRingtonePlayer(mediaPlayer, audioAttributes, audioManager,
                    /* hapticGenerator= */ null, volumeShaper);
        } catch (SecurityException | IOException e) {
            Log.e(TAG, "Failed to open fallback ringtone");
            mediaPlayer.release();
            return null;
        } finally {
            Trace.endSection();
        }
    }

    @Override
    public boolean play() {
        // Play ringtones if stream volume is over 0 or if it is a haptic-only ringtone
        // (typically because ringer mode is vibrate).
        if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes))
                == 0 && (mAudioAttributes.areHapticChannelsMuted() || !hasHapticChannels())) {
            return true;  // Successfully played while muted.
        }
        synchronized (sActiveRingtones) {
            sActiveRingtones.add(this);
        }

        mMediaPlayer.setOnCompletionListener(this);
        mMediaPlayer.start();
        if (mVolumeShaper != null) {
            mVolumeShaper.apply(VolumeShaper.Operation.PLAY);
        }
        return true;
    }

    @Override
    public boolean isPlaying() {
        return mMediaPlayer.isPlaying();
    }

    @Override
    public void stopAndRelease() {
        synchronized (sActiveRingtones) {
            sActiveRingtones.remove(this);
        }
        if (mHapticGenerator != null) {
            mHapticGenerator.release();
        }
        mMediaPlayer.setOnCompletionListener(null);
        mMediaPlayer.reset();
        mMediaPlayer.release();
    }

    @Override
    public void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) {
        mMediaPlayer.setPreferredDevice(audioDeviceInfo);
    }

    @Override
    public void setLooping(boolean looping) {
        mMediaPlayer.setLooping(looping);
    }

    @Override
    public void setHapticGeneratorEnabled(boolean enabled) {
        if (enabled && mHapticGenerator == null) {
            mHapticGenerator = HapticGenerator.create(mMediaPlayer.getAudioSessionId());
        }
        if (mHapticGenerator != null) {
            mHapticGenerator.setEnabled(enabled);
        }
    }

    @Override
    public void setVolume(float volume) {
        mMediaPlayer.setVolume(volume);
    }

    /**
     * Same as AudioManager.hasHapticChannels except it assumes an already created ringtone.
     * @hide
     */
    @Override
    public boolean hasHapticChannels() {
        // FIXME: support remote player, or internalize haptic channels support and remove entirely.
        try {
            Trace.beginSection("LocalRingtonePlayer.hasHapticChannels");
            for (MediaPlayer.TrackInfo trackInfo : mMediaPlayer.getTrackInfo()) {
                if (trackInfo.hasHapticChannels()) {
                    return true;
                }
            }
        } finally {
            Trace.endSection();
        }
        return false;
    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        synchronized (sActiveRingtones) {
            sActiveRingtones.remove(this);
        }
        mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle.
    }
}
+230 −223

File changed.

Preview size limit exceeded, changes collapsed.

+3 −7
Original line number Diff line number Diff line
@@ -16,7 +16,6 @@

package android.media;

import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -39,10 +38,7 @@ import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.FileUtils;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
@@ -724,7 +720,7 @@ public class RingtoneManager {
                volumeShaperConfig, false);
        if (ringtone != null) {
            ringtone.setAudioAttributesField(audioAttributes);
            if (!ringtone.createLocalMediaPlayer()) {
            if (!ringtone.reinitializeActivePlayer()) {
                Log.e(TAG, "Failed to open ringtone " + ringtoneUri);
                return null;
            }
@@ -743,7 +739,7 @@ public class RingtoneManager {
     *            not be set (and the default used instead).
     * @param createLocalMediaPlayer when true, the ringtone returned will be fully
     *      created otherwise, it will require the caller to create the media player manually
     *      {@link Ringtone#createLocalMediaPlayer()} in order to play the Ringtone.
     *      {@link Ringtone#reinitializeActivePlayer()} in order to play the Ringtone.
     * @see #getRingtone(Context, Uri)
     */
    @UnsupportedAppUsage
@@ -766,7 +762,7 @@ public class RingtoneManager {
            r.setVolumeShaperConfig(volumeShaperConfig);
            r.setUri(ringtoneUri, volumeShaperConfig);
            if (createLocalMediaPlayer) {
                if (!r.createLocalMediaPlayer()) {
                if (!r.reinitializeActivePlayer()) {
                    Log.e(TAG, "Failed to open ringtone " + ringtoneUri);
                    return null;
                }
+30 −8
Original line number Diff line number Diff line
@@ -99,7 +99,7 @@ public class RingtonePlayer implements CoreStartable {
            mRingtone = new Ringtone(getContextForUser(user), false);
            mRingtone.setAudioAttributesField(aa);
            mRingtone.setUri(uri, volumeShaperConfig);
            mRingtone.createLocalMediaPlayer();
            mRingtone.reinitializeActivePlayer();
        }

        @Override
@@ -116,11 +116,14 @@ public class RingtonePlayer implements CoreStartable {
        @Override
        public void play(IBinder token, Uri uri, AudioAttributes aa, float volume, boolean looping)
                throws RemoteException {
            playWithVolumeShaping(token, uri, aa, volume, looping, null);
            playWithVolumeShaping(token, uri, aa, volume, looping, /* hapticGenerator= */ false,
                    null);
        }

        @Override
        public void playWithVolumeShaping(IBinder token, Uri uri, AudioAttributes aa, float volume,
                boolean looping, @Nullable VolumeShaper.Configuration volumeShaperConfig)
                boolean looping, boolean isHapticGeneratorEnabled,
                @Nullable VolumeShaper.Configuration volumeShaperConfig)
                throws RemoteException {
            if (LOGD) {
                Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid="
@@ -138,6 +141,7 @@ public class RingtonePlayer implements CoreStartable {
            }
            client.mRingtone.setLooping(looping);
            client.mRingtone.setVolume(volume);
            client.mRingtone.setHapticGeneratorEnabled(isHapticGeneratorEnabled);
            client.mRingtone.play();
        }

@@ -169,18 +173,36 @@ public class RingtonePlayer implements CoreStartable {
        }

        @Override
        public void setPlaybackProperties(IBinder token, float volume, boolean looping,
                boolean hapticGeneratorEnabled) {
        public void setHapticGeneratorEnabled(IBinder token, boolean hapticGeneratorEnabled) {
            Client client;
            synchronized (mClients) {
                client = mClients.get(token);
            }
            if (client != null) {
                client.mRingtone.setVolume(volume);
                client.mRingtone.setLooping(looping);
                client.mRingtone.setHapticGeneratorEnabled(hapticGeneratorEnabled);
            }
            // else no client for token when setting playback properties but will be set at play()
        }

        @Override
        public void setLooping(IBinder token, boolean looping) {
            Client client;
            synchronized (mClients) {
                client = mClients.get(token);
            }
            if (client != null) {
                client.mRingtone.setLooping(looping);
            }
        }

        @Override
        public void setVolume(IBinder token, float volume) {
            Client client;
            synchronized (mClients) {
                client = mClients.get(token);
            }
            if (client != null) {
                client.mRingtone.setVolume(volume);
            }
        }

        @Override