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.ContentProvider; 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.content.pm.PackageManager.NameNotFoundException; 24 import android.database.Cursor; 25 import android.media.AudioAttributes; 26 import android.media.IAudioService; 27 import android.media.IRingtonePlayer; 28 import android.media.Ringtone; 29 import android.media.VolumeShaper; 30 import android.net.Uri; 31 import android.os.Binder; 32 import android.os.IBinder; 33 import android.os.ParcelFileDescriptor; 34 import android.os.Process; 35 import android.os.RemoteException; 36 import android.os.ServiceManager; 37 import android.os.UserHandle; 38 import android.provider.MediaStore; 39 import android.util.Log; 40 41 import com.android.systemui.CoreStartable; 42 import com.android.systemui.dagger.SysUISingleton; 43 44 import java.io.IOException; 45 import java.io.PrintWriter; 46 import java.util.HashMap; 47 48 import javax.inject.Inject; 49 50 /** 51 * Service that offers to play ringtones by {@link Uri}, since our process has 52 * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}. 53 */ 54 @SysUISingleton 55 public class RingtonePlayer implements CoreStartable { 56 private static final String TAG = "RingtonePlayer"; 57 private static final boolean LOGD = true; 58 private final Context mContext; 59 60 // TODO: support Uri switching under same IBinder 61 62 private IAudioService mAudioService; 63 64 private final NotificationPlayer mAsyncPlayer = new NotificationPlayer(TAG); 65 private final HashMap<IBinder, Client> mClients = new HashMap<IBinder, Client>(); 66 67 @Inject RingtonePlayer(Context context)68 public RingtonePlayer(Context context) { 69 mContext = context; 70 } 71 72 @Override start()73 public void start() { 74 mAsyncPlayer.setUsesWakeLock(mContext); 75 76 mAudioService = IAudioService.Stub.asInterface( 77 ServiceManager.getService(Context.AUDIO_SERVICE)); 78 try { 79 mAudioService.setRingtonePlayer(mCallback); 80 } catch (RemoteException e) { 81 Log.e(TAG, "Problem registering RingtonePlayer: " + e); 82 } 83 } 84 85 /** 86 * Represents an active remote {@link Ringtone} client. 87 */ 88 private class Client implements IBinder.DeathRecipient { 89 private final IBinder mToken; 90 private final Ringtone mRingtone; 91 Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa)92 public Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa) { 93 this(token, uri, user, aa, null); 94 } 95 Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa, @Nullable VolumeShaper.Configuration volumeShaperConfig)96 Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa, 97 @Nullable VolumeShaper.Configuration volumeShaperConfig) { 98 mToken = token; 99 100 mRingtone = new Ringtone(getContextForUser(user), false); 101 mRingtone.setAudioAttributesField(aa); 102 mRingtone.setUri(uri, volumeShaperConfig); 103 mRingtone.createLocalMediaPlayer(); 104 } 105 106 @Override binderDied()107 public void binderDied() { 108 if (LOGD) Log.d(TAG, "binderDied() token=" + mToken); 109 synchronized (mClients) { 110 mClients.remove(mToken); 111 } 112 mRingtone.stop(); 113 } 114 } 115 116 private IRingtonePlayer mCallback = new IRingtonePlayer.Stub() { 117 @Override 118 public void play(IBinder token, Uri uri, AudioAttributes aa, float volume, boolean looping) 119 throws RemoteException { 120 playWithVolumeShaping(token, uri, aa, volume, looping, null); 121 } 122 @Override 123 public void playWithVolumeShaping(IBinder token, Uri uri, AudioAttributes aa, float volume, 124 boolean looping, @Nullable VolumeShaper.Configuration volumeShaperConfig) 125 throws RemoteException { 126 if (LOGD) { 127 Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid=" 128 + Binder.getCallingUid() + ")"); 129 } 130 enforceUriUserId(uri); 131 132 Client client; 133 synchronized (mClients) { 134 client = mClients.get(token); 135 if (client == null) { 136 final UserHandle user = Binder.getCallingUserHandle(); 137 client = new Client(token, uri, user, aa, volumeShaperConfig); 138 token.linkToDeath(client, 0); 139 mClients.put(token, client); 140 } 141 } 142 client.mRingtone.setLooping(looping); 143 client.mRingtone.setVolume(volume); 144 client.mRingtone.play(); 145 } 146 147 @Override 148 public void stop(IBinder token) { 149 if (LOGD) Log.d(TAG, "stop(token=" + token + ")"); 150 Client client; 151 synchronized (mClients) { 152 client = mClients.remove(token); 153 } 154 if (client != null) { 155 client.mToken.unlinkToDeath(client, 0); 156 client.mRingtone.stop(); 157 } 158 } 159 160 @Override 161 public boolean isPlaying(IBinder token) { 162 if (LOGD) Log.d(TAG, "isPlaying(token=" + token + ")"); 163 Client client; 164 synchronized (mClients) { 165 client = mClients.get(token); 166 } 167 if (client != null) { 168 return client.mRingtone.isPlaying(); 169 } else { 170 return false; 171 } 172 } 173 174 @Override 175 public void setPlaybackProperties(IBinder token, float volume, boolean looping, 176 boolean hapticGeneratorEnabled) { 177 Client client; 178 synchronized (mClients) { 179 client = mClients.get(token); 180 } 181 if (client != null) { 182 client.mRingtone.setVolume(volume); 183 client.mRingtone.setLooping(looping); 184 client.mRingtone.setHapticGeneratorEnabled(hapticGeneratorEnabled); 185 } 186 // else no client for token when setting playback properties but will be set at play() 187 } 188 189 @Override 190 public void playAsync(Uri uri, UserHandle user, boolean looping, AudioAttributes aa, 191 float volume) { 192 if (LOGD) Log.d(TAG, "playAsync(uri=" + uri + ", user=" + user + ")"); 193 if (Binder.getCallingUid() != Process.SYSTEM_UID) { 194 throw new SecurityException("Async playback only available from system UID."); 195 } 196 if (UserHandle.ALL.equals(user)) { 197 user = UserHandle.SYSTEM; 198 } 199 mAsyncPlayer.play(getContextForUser(user), uri, looping, aa, volume); 200 } 201 202 @Override 203 public void stopAsync() { 204 if (LOGD) Log.d(TAG, "stopAsync()"); 205 if (Binder.getCallingUid() != Process.SYSTEM_UID) { 206 throw new SecurityException("Async playback only available from system UID."); 207 } 208 mAsyncPlayer.stop(); 209 } 210 211 @Override 212 public String getTitle(Uri uri) { 213 enforceUriUserId(uri); 214 final UserHandle user = Binder.getCallingUserHandle(); 215 return Ringtone.getTitle(getContextForUser(user), uri, 216 false /*followSettingsUri*/, false /*allowRemote*/); 217 } 218 219 @Override 220 public ParcelFileDescriptor openRingtone(Uri uri) { 221 enforceUriUserId(uri); 222 final UserHandle user = Binder.getCallingUserHandle(); 223 final ContentResolver resolver = getContextForUser(user).getContentResolver(); 224 225 // Only open the requested Uri if it's a well-known ringtone or 226 // other sound from the platform media store, otherwise this opens 227 // up arbitrary access to any file on external storage. 228 if (uri.toString().startsWith(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) { 229 try (Cursor c = resolver.query(uri, new String[] { 230 MediaStore.Audio.AudioColumns.IS_RINGTONE, 231 MediaStore.Audio.AudioColumns.IS_ALARM, 232 MediaStore.Audio.AudioColumns.IS_NOTIFICATION 233 }, null, null, null)) { 234 if (c.moveToFirst()) { 235 if (c.getInt(0) != 0 || c.getInt(1) != 0 || c.getInt(2) != 0) { 236 try { 237 return resolver.openFileDescriptor(uri, "r"); 238 } catch (IOException e) { 239 throw new SecurityException(e); 240 } 241 } 242 } 243 } 244 } 245 throw new SecurityException("Uri is not ringtone, alarm, or notification: " + uri); 246 } 247 }; 248 249 /** 250 * Must be called from the Binder calling thread. 251 * Ensures caller is from the same userId as the content they're trying to access. 252 * @param uri the URI to check 253 * @throws SecurityException when in a non-system call and userId in uri differs from the 254 * caller's userId 255 */ enforceUriUserId(Uri uri)256 private void enforceUriUserId(Uri uri) throws SecurityException { 257 final int uriUserId = ContentProvider.getUserIdFromUri(uri, UserHandle.myUserId()); 258 // for a non-system call, verify the URI to play belongs to the same user as the caller 259 if (UserHandle.isApp(Binder.getCallingUid()) && (UserHandle.myUserId() != uriUserId)) { 260 final String errorMessage = "Illegal access to uri=" + uri 261 + " content associated with user=" + uriUserId 262 + ", current userID: " + UserHandle.myUserId(); 263 if (android.media.audio.Flags.ringtoneUserUriCheck()) { 264 throw new SecurityException(errorMessage); 265 } else { 266 Log.e(TAG, errorMessage, new Exception()); 267 } 268 } 269 } 270 getContextForUser(UserHandle user)271 private Context getContextForUser(UserHandle user) { 272 try { 273 return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user); 274 } catch (NameNotFoundException e) { 275 throw new RuntimeException(e); 276 } 277 } 278 279 @Override dump(PrintWriter pw, String[] args)280 public void dump(PrintWriter pw, String[] args) { 281 pw.println("Clients:"); 282 synchronized (mClients) { 283 for (Client client : mClients.values()) { 284 pw.print(" mToken="); 285 pw.print(client.mToken); 286 pw.print(" mUri="); 287 pw.println(client.mRingtone.getUri()); 288 } 289 } 290 } 291 } 292