1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.media; 18 19 import android.annotation.Nullable; 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.content.pm.PackageManager.NameNotFoundException; 23 import android.database.Cursor; 24 import android.media.AudioAttributes; 25 import android.media.IAudioService; 26 import android.media.IRingtonePlayer; 27 import android.media.Ringtone; 28 import android.media.VolumeShaper; 29 import android.net.Uri; 30 import android.os.Binder; 31 import android.os.IBinder; 32 import android.os.ParcelFileDescriptor; 33 import android.os.Process; 34 import android.os.RemoteException; 35 import android.os.ServiceManager; 36 import android.os.UserHandle; 37 import android.provider.MediaStore; 38 import android.util.Log; 39 40 import com.android.systemui.CoreStartable; 41 import com.android.systemui.dagger.SysUISingleton; 42 43 import java.io.IOException; 44 import java.io.PrintWriter; 45 import java.util.HashMap; 46 47 import javax.inject.Inject; 48 49 /** 50 * Service that offers to play ringtones by {@link Uri}, since our process has 51 * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}. 52 */ 53 @SysUISingleton 54 public class RingtonePlayer implements CoreStartable { 55 private static final String TAG = "RingtonePlayer"; 56 private static final boolean LOGD = false; 57 private final Context mContext; 58 59 // TODO: support Uri switching under same IBinder 60 61 private IAudioService mAudioService; 62 63 private final NotificationPlayer mAsyncPlayer = new NotificationPlayer(TAG); 64 private final HashMap<IBinder, Client> mClients = new HashMap<IBinder, Client>(); 65 66 @Inject RingtonePlayer(Context context)67 public RingtonePlayer(Context context) { 68 mContext = context; 69 } 70 71 @Override start()72 public void start() { 73 mAsyncPlayer.setUsesWakeLock(mContext); 74 75 mAudioService = IAudioService.Stub.asInterface( 76 ServiceManager.getService(Context.AUDIO_SERVICE)); 77 try { 78 mAudioService.setRingtonePlayer(mCallback); 79 } catch (RemoteException e) { 80 Log.e(TAG, "Problem registering RingtonePlayer: " + e); 81 } 82 } 83 84 /** 85 * Represents an active remote {@link Ringtone} client. 86 */ 87 private class Client implements IBinder.DeathRecipient { 88 private final IBinder mToken; 89 private final Ringtone mRingtone; 90 Client(IBinder token, Ringtone ringtone)91 Client(IBinder token, Ringtone ringtone) { 92 mToken = token; 93 mRingtone = ringtone; 94 } 95 96 @Override binderDied()97 public void binderDied() { 98 if (LOGD) Log.d(TAG, "binderDied() token=" + mToken); 99 synchronized (mClients) { 100 mClients.remove(mToken); 101 } 102 mRingtone.stop(); 103 } 104 } 105 106 private IRingtonePlayer mCallback = new IRingtonePlayer.Stub() { 107 @Override 108 public void play(IBinder token, Uri uri, AudioAttributes aa, float volume, boolean looping) 109 throws RemoteException { 110 playWithVolumeShaping(token, uri, aa, volume, looping, null); 111 } 112 @Override 113 public void playWithVolumeShaping(IBinder token, Uri uri, AudioAttributes aa, float volume, 114 boolean looping, @Nullable VolumeShaper.Configuration volumeShaperConfig) 115 throws RemoteException { 116 if (LOGD) { 117 Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid=" 118 + Binder.getCallingUid() + ")"); 119 } 120 Client client; 121 synchronized (mClients) { 122 client = mClients.get(token); 123 } 124 // Don't hold the lock while constructing the ringtone, since it can be slow. The caller 125 // shouldn't call play on the same ringtone from 2 threads, so this shouldn't race and 126 // waste the build. 127 if (client == null) { 128 final UserHandle user = Binder.getCallingUserHandle(); 129 Ringtone ringtone = new Ringtone(getContextForUser(user), false); 130 ringtone.setAudioAttributesField(aa); 131 ringtone.setUri(uri, volumeShaperConfig); 132 ringtone.createLocalMediaPlayer(); 133 synchronized (mClients) { 134 client = mClients.get(token); 135 if (client == null) { 136 client = new Client(token, ringtone); 137 token.linkToDeath(client, 0); 138 mClients.put(token, client); 139 ringtone = null; // "owned" by the client now. 140 } 141 } 142 // Clean up ringtone if it was abandoned (a client already existed). 143 if (ringtone != null) { 144 ringtone.stop(); 145 } 146 } 147 client.mRingtone.setLooping(looping); 148 client.mRingtone.setVolume(volume); 149 client.mRingtone.play(); 150 } 151 152 @Override 153 public void stop(IBinder token) { 154 if (LOGD) Log.d(TAG, "stop(token=" + token + ")"); 155 Client client; 156 synchronized (mClients) { 157 client = mClients.remove(token); 158 } 159 if (client != null) { 160 client.mToken.unlinkToDeath(client, 0); 161 client.mRingtone.stop(); 162 } 163 } 164 165 @Override 166 public boolean isPlaying(IBinder token) { 167 if (LOGD) Log.d(TAG, "isPlaying(token=" + token + ")"); 168 Client client; 169 synchronized (mClients) { 170 client = mClients.get(token); 171 } 172 if (client != null) { 173 return client.mRingtone.isPlaying(); 174 } else { 175 return false; 176 } 177 } 178 179 @Override 180 public void setPlaybackProperties(IBinder token, float volume, boolean looping, 181 boolean hapticGeneratorEnabled) { 182 Client client; 183 synchronized (mClients) { 184 client = mClients.get(token); 185 } 186 if (client != null) { 187 client.mRingtone.setVolume(volume); 188 client.mRingtone.setLooping(looping); 189 client.mRingtone.setHapticGeneratorEnabled(hapticGeneratorEnabled); 190 } 191 // else no client for token when setting playback properties but will be set at play() 192 } 193 194 @Override 195 public void playAsync(Uri uri, UserHandle user, boolean looping, AudioAttributes aa) { 196 if (LOGD) Log.d(TAG, "playAsync(uri=" + uri + ", user=" + user + ")"); 197 if (Binder.getCallingUid() != Process.SYSTEM_UID) { 198 throw new SecurityException("Async playback only available from system UID."); 199 } 200 if (UserHandle.ALL.equals(user)) { 201 user = UserHandle.SYSTEM; 202 } 203 mAsyncPlayer.play(getContextForUser(user), uri, looping, aa); 204 } 205 206 @Override 207 public void stopAsync() { 208 if (LOGD) Log.d(TAG, "stopAsync()"); 209 if (Binder.getCallingUid() != Process.SYSTEM_UID) { 210 throw new SecurityException("Async playback only available from system UID."); 211 } 212 mAsyncPlayer.stop(); 213 } 214 215 @Override 216 public String getTitle(Uri uri) { 217 final UserHandle user = Binder.getCallingUserHandle(); 218 return Ringtone.getTitle(getContextForUser(user), uri, 219 false /*followSettingsUri*/, false /*allowRemote*/); 220 } 221 222 @Override 223 public ParcelFileDescriptor openRingtone(Uri uri) { 224 final UserHandle user = Binder.getCallingUserHandle(); 225 final ContentResolver resolver = getContextForUser(user).getContentResolver(); 226 227 // Only open the requested Uri if it's a well-known ringtone or 228 // other sound from the platform media store, otherwise this opens 229 // up arbitrary access to any file on external storage. 230 if (uri.toString().startsWith(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) { 231 try (Cursor c = resolver.query(uri, new String[] { 232 MediaStore.Audio.AudioColumns.IS_RINGTONE, 233 MediaStore.Audio.AudioColumns.IS_ALARM, 234 MediaStore.Audio.AudioColumns.IS_NOTIFICATION 235 }, null, null, null)) { 236 if (c.moveToFirst()) { 237 if (c.getInt(0) != 0 || c.getInt(1) != 0 || c.getInt(2) != 0) { 238 try { 239 return resolver.openFileDescriptor(uri, "r"); 240 } catch (IOException e) { 241 throw new SecurityException(e); 242 } 243 } 244 } 245 } 246 } 247 throw new SecurityException("Uri is not ringtone, alarm, or notification: " + uri); 248 } 249 }; 250 getContextForUser(UserHandle user)251 private Context getContextForUser(UserHandle user) { 252 try { 253 return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user); 254 } catch (NameNotFoundException e) { 255 throw new RuntimeException(e); 256 } 257 } 258 259 @Override dump(PrintWriter pw, String[] args)260 public void dump(PrintWriter pw, String[] args) { 261 pw.println("Clients:"); 262 synchronized (mClients) { 263 for (Client client : mClients.values()) { 264 pw.print(" mToken="); 265 pw.print(client.mToken); 266 pw.print(" mUri="); 267 pw.println(client.mRingtone.getUri()); 268 } 269 } 270 } 271 } 272