blob: 7a488365c740b62cadb6d880e47c36e0aa31d3fc [file] [log] [blame]
/*
* Copyright (C) 2012 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 com.android.systemui.media;
import static java.util.Objects.requireNonNull;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import android.database.Cursor;
import android.media.AudioAttributes;
import android.media.IAudioService;
import android.media.IRingtonePlayer;
import android.media.Ringtone;
import android.media.VolumeShaper;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.VibrationEffect;
import android.os.vibrator.Flags;
import android.provider.MediaStore;
import android.util.Log;
import com.android.systemui.CoreStartable;
import com.android.systemui.dagger.SysUISingleton;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import javax.inject.Inject;
/**
* Service that offers to play ringtones by {@link Uri}, since our process has
* {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}.
*/
@SysUISingleton
public class RingtonePlayer implements CoreStartable {
private static final String TAG = "RingtonePlayer";
private static final boolean LOGD = true;
private final Context mContext;
// TODO: support Uri switching under same IBinder
private IAudioService mAudioService;
private final NotificationPlayer mAsyncPlayer = new NotificationPlayer(TAG);
private final HashMap<IBinder, Client> mClients = new HashMap<IBinder, Client>();
@Inject
public RingtonePlayer(Context context) {
mContext = context;
}
@Override
public void start() {
mAsyncPlayer.setUsesWakeLock(mContext);
mAudioService = IAudioService.Stub.asInterface(
ServiceManager.getService(Context.AUDIO_SERVICE));
try {
mAudioService.setRingtonePlayer(mCallback);
} catch (RemoteException e) {
Log.e(TAG, "Problem registering RingtonePlayer: " + e);
}
}
/**
* Represents an active remote {@link Ringtone} client.
*/
private class Client implements IBinder.DeathRecipient {
private final IBinder mToken;
private Ringtone mRingtone;
Client(@NonNull IBinder token, @NonNull Ringtone ringtone) {
mToken = requireNonNull(token);
mRingtone = requireNonNull(ringtone);
}
@Override
public void binderDied() {
if (LOGD) Log.d(TAG, "binderDied() token=" + mToken);
synchronized (mClients) {
mClients.remove(mToken);
}
mRingtone.stop();
}
}
private IRingtonePlayer mCallback = new IRingtonePlayer.Stub() {
@Override
public void play(IBinder token, Uri uri, AudioAttributes aa, float volume, boolean looping)
throws RemoteException {
if (Flags.hapticsCustomizationRingtoneV2Enabled()) {
playRemoteRingtone(token, uri, aa, true, Ringtone.MEDIA_SOUND,
null, volume, looping, /* hapticGenerator= */ false,
null);
} else {
playWithVolumeShaping(token, uri, aa, volume, looping, null);
}
}
@Override
public void playWithVolumeShaping(
IBinder token, Uri uri, AudioAttributes aa, float volume,
boolean looping, @Nullable VolumeShaper.Configuration volumeShaperConfig)
throws RemoteException {
if (LOGD) {
Log.d(TAG, "playWithVolumeShaping(token=" + token + ", uri=" + uri + ", uid="
+ Binder.getCallingUid() + ")");
}
Client client;
synchronized (mClients) {
client = mClients.get(token);
}
// Don't hold the lock while constructing the ringtone, since it can be slow. The caller
// shouldn't call play on the same ringtone from 2 threads, so this shouldn't race and
// waste the build.
if (client == null) {
final UserHandle user = Binder.getCallingUserHandle();
Ringtone ringtone = Ringtone.createV1WithCustomAudioAttributes(
getContextForUser(user), aa, uri, volumeShaperConfig,
/* allowRemote= */ false);
synchronized (mClients) {
client = mClients.get(token);
if (client == null) {
client = new Client(token, ringtone);
token.linkToDeath(client, 0);
mClients.put(token, client);
ringtone = null; // "owned" by the client now.
}
}
// Clean up ringtone if it was abandoned (a client already existed).
if (ringtone != null) {
ringtone.stop();
}
}
client.mRingtone.setLooping(looping);
client.mRingtone.setVolume(volume);
client.mRingtone.play();
}
@Override
public void playRemoteRingtone(IBinder token, Uri uri, AudioAttributes aa,
boolean useExactAudioAttributes,
@Ringtone.RingtoneMedia int enabledMedia, @Nullable VibrationEffect vibrationEffect,
float volume,
boolean looping, boolean isHapticGeneratorEnabled,
@Nullable VolumeShaper.Configuration volumeShaperConfig)
throws RemoteException {
if (LOGD) {
Log.d(TAG, "playRemoteRingtone(token=" + token + ", uri=" + uri + ", uid="
+ Binder.getCallingUid() + ")");
}
// Don't hold the lock while constructing the ringtone, since it can be slow. The caller
// shouldn't call play on the same ringtone from 2 threads, so this shouldn't race and
// waste the build.
Client client;
synchronized (mClients) {
client = mClients.get(token);
}
if (client == null) {
final UserHandle user = Binder.getCallingUserHandle();
Ringtone ringtone = new Ringtone.Builder(getContextForUser(user), enabledMedia, aa)
.setLocalOnly()
.setUri(uri)
.setLooping(looping)
.setInitialSoundVolume(volume)
.setUseExactAudioAttributes(useExactAudioAttributes)
.setEnableHapticGenerator(isHapticGeneratorEnabled)
.setVibrationEffect(vibrationEffect)
.setVolumeShaperConfig(volumeShaperConfig)
.build();
if (ringtone == null) {
return;
}
synchronized (mClients) {
client = mClients.get(token);
if (client == null) {
client = new Client(token, ringtone);
token.linkToDeath(client, 0);
mClients.put(token, client);
}
}
}
// Ensure the client is initialized outside the all-clients lock, as it can be slow.
client.mRingtone.play();
}
@Override
public void stop(IBinder token) {
if (LOGD) Log.d(TAG, "stop(token=" + token + ")");
Client client;
synchronized (mClients) {
client = mClients.remove(token);
}
if (client != null) {
client.mToken.unlinkToDeath(client, 0);
client.mRingtone.stop();
}
}
@Override
public boolean isPlaying(IBinder token) {
if (LOGD) Log.d(TAG, "isPlaying(token=" + token + ")");
Client client;
synchronized (mClients) {
client = mClients.get(token);
}
if (client != null) {
return client.mRingtone.isPlaying();
} else {
return false;
}
}
@Override
public void setPlaybackProperties(IBinder token, float volume, boolean looping,
boolean hapticGeneratorEnabled) {
// RingtoneV1-exclusive path.
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 setHapticGeneratorEnabled(IBinder token, boolean hapticGeneratorEnabled) {
Client client;
synchronized (mClients) {
client = mClients.get(token);
}
if (client != null) {
client.mRingtone.setHapticGeneratorEnabled(hapticGeneratorEnabled);
}
}
@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
public void playAsync(Uri uri, UserHandle user, boolean looping, AudioAttributes aa,
float volume) {
if (LOGD) Log.d(TAG, "playAsync(uri=" + uri + ", user=" + user + ")");
if (Binder.getCallingUid() != Process.SYSTEM_UID) {
throw new SecurityException("Async playback only available from system UID.");
}
if (UserHandle.ALL.equals(user)) {
user = UserHandle.SYSTEM;
}
mAsyncPlayer.play(getContextForUser(user), uri, looping, aa, volume);
}
@Override
public void stopAsync() {
if (LOGD) Log.d(TAG, "stopAsync()");
if (Binder.getCallingUid() != Process.SYSTEM_UID) {
throw new SecurityException("Async playback only available from system UID.");
}
mAsyncPlayer.stop();
}
@Override
public String getTitle(Uri uri) {
final UserHandle user = Binder.getCallingUserHandle();
return Ringtone.getTitle(getContextForUser(user), uri,
false /*followSettingsUri*/, false /*allowRemote*/);
}
@Override
public ParcelFileDescriptor openRingtone(Uri uri) {
final UserHandle user = Binder.getCallingUserHandle();
final ContentResolver resolver = getContextForUser(user).getContentResolver();
// Only open the requested Uri if it's a well-known ringtone or
// other sound from the platform media store, otherwise this opens
// up arbitrary access to any file on external storage.
if (uri.toString().startsWith(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) {
try (Cursor c = resolver.query(uri, new String[] {
MediaStore.Audio.AudioColumns.IS_RINGTONE,
MediaStore.Audio.AudioColumns.IS_ALARM,
MediaStore.Audio.AudioColumns.IS_NOTIFICATION
}, null, null, null)) {
if (c.moveToFirst()) {
if (c.getInt(0) != 0 || c.getInt(1) != 0 || c.getInt(2) != 0) {
try {
return resolver.openFileDescriptor(uri, "r");
} catch (IOException e) {
throw new SecurityException(e);
}
}
}
}
}
throw new SecurityException("Uri is not ringtone, alarm, or notification: " + uri);
}
};
private Context getContextForUser(UserHandle user) {
try {
return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
} catch (NameNotFoundException e) {
throw new RuntimeException(e);
}
}
@Override
public void dump(PrintWriter pw, String[] args) {
pw.println("Clients:");
synchronized (mClients) {
for (Client client : mClients.values()) {
pw.print(" mToken=");
pw.print(client.mToken);
pw.print(" mUri=");
pw.println(client.mRingtone.getUri());
}
}
}
}