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.SystemUI; 41 42 import java.io.FileDescriptor; 43 import java.io.IOException; 44 import java.io.PrintWriter; 45 import java.util.HashMap; 46 47 /** 48 * Service that offers to play ringtones by {@link Uri}, since our process has 49 * {@link android.Manifest.permission#READ_EXTERNAL_STORAGE}. 50 */ 51 public class RingtonePlayer extends SystemUI { 52 private static final String TAG = "RingtonePlayer"; 53 private static final boolean LOGD = false; 54 55 // TODO: support Uri switching under same IBinder 56 57 private IAudioService mAudioService; 58 59 private final NotificationPlayer mAsyncPlayer = new NotificationPlayer(TAG); 60 private final HashMap<IBinder, Client> mClients = new HashMap<IBinder, Client>(); 61 RingtonePlayer(Context context)62 public RingtonePlayer(Context context) { 63 super(context); 64 } 65 66 @Override start()67 public void start() { 68 mAsyncPlayer.setUsesWakeLock(mContext); 69 70 mAudioService = IAudioService.Stub.asInterface( 71 ServiceManager.getService(Context.AUDIO_SERVICE)); 72 try { 73 mAudioService.setRingtonePlayer(mCallback); 74 } catch (RemoteException e) { 75 Log.e(TAG, "Problem registering RingtonePlayer: " + e); 76 } 77 } 78 79 /** 80 * Represents an active remote {@link Ringtone} client. 81 */ 82 private class Client implements IBinder.DeathRecipient { 83 private final IBinder mToken; 84 private final Ringtone mRingtone; 85 Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa)86 public Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa) { 87 this(token, uri, user, aa, null); 88 } 89 Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa, @Nullable VolumeShaper.Configuration volumeShaperConfig)90 Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa, 91 @Nullable VolumeShaper.Configuration volumeShaperConfig) { 92 mToken = token; 93 94 mRingtone = new Ringtone(getContextForUser(user), false); 95 mRingtone.setAudioAttributes(aa); 96 mRingtone.setUri(uri, volumeShaperConfig); 97 } 98 99 @Override binderDied()100 public void binderDied() { 101 if (LOGD) Log.d(TAG, "binderDied() token=" + mToken); 102 synchronized (mClients) { 103 mClients.remove(mToken); 104 } 105 mRingtone.stop(); 106 } 107 } 108 109 private IRingtonePlayer mCallback = new IRingtonePlayer.Stub() { 110 @Override 111 public void play(IBinder token, Uri uri, AudioAttributes aa, float volume, boolean looping) 112 throws RemoteException { 113 playWithVolumeShaping(token, uri, aa, volume, looping, null); 114 } 115 @Override 116 public void playWithVolumeShaping(IBinder token, Uri uri, AudioAttributes aa, float volume, 117 boolean looping, @Nullable VolumeShaper.Configuration volumeShaperConfig) 118 throws RemoteException { 119 if (LOGD) { 120 Log.d(TAG, "play(token=" + token + ", uri=" + uri + ", uid=" 121 + Binder.getCallingUid() + ")"); 122 } 123 Client client; 124 synchronized (mClients) { 125 client = mClients.get(token); 126 if (client == null) { 127 final UserHandle user = Binder.getCallingUserHandle(); 128 client = new Client(token, uri, user, aa, volumeShaperConfig); 129 token.linkToDeath(client, 0); 130 mClients.put(token, client); 131 } 132 } 133 client.mRingtone.setLooping(looping); 134 client.mRingtone.setVolume(volume); 135 client.mRingtone.play(); 136 } 137 138 @Override 139 public void stop(IBinder token) { 140 if (LOGD) Log.d(TAG, "stop(token=" + token + ")"); 141 Client client; 142 synchronized (mClients) { 143 client = mClients.remove(token); 144 } 145 if (client != null) { 146 client.mToken.unlinkToDeath(client, 0); 147 client.mRingtone.stop(); 148 } 149 } 150 151 @Override 152 public boolean isPlaying(IBinder token) { 153 if (LOGD) Log.d(TAG, "isPlaying(token=" + token + ")"); 154 Client client; 155 synchronized (mClients) { 156 client = mClients.get(token); 157 } 158 if (client != null) { 159 return client.mRingtone.isPlaying(); 160 } else { 161 return false; 162 } 163 } 164 165 @Override 166 public void setPlaybackProperties(IBinder token, float volume, boolean looping, 167 boolean hapticGeneratorEnabled) { 168 Client client; 169 synchronized (mClients) { 170 client = mClients.get(token); 171 } 172 if (client != null) { 173 client.mRingtone.setVolume(volume); 174 client.mRingtone.setLooping(looping); 175 client.mRingtone.setHapticGeneratorEnabled(hapticGeneratorEnabled); 176 } 177 // else no client for token when setting playback properties but will be set at play() 178 } 179 180 @Override 181 public void playAsync(Uri uri, UserHandle user, boolean looping, AudioAttributes aa) { 182 if (LOGD) Log.d(TAG, "playAsync(uri=" + uri + ", user=" + user + ")"); 183 if (Binder.getCallingUid() != Process.SYSTEM_UID) { 184 throw new SecurityException("Async playback only available from system UID."); 185 } 186 if (UserHandle.ALL.equals(user)) { 187 user = UserHandle.SYSTEM; 188 } 189 mAsyncPlayer.play(getContextForUser(user), uri, looping, aa); 190 } 191 192 @Override 193 public void stopAsync() { 194 if (LOGD) Log.d(TAG, "stopAsync()"); 195 if (Binder.getCallingUid() != Process.SYSTEM_UID) { 196 throw new SecurityException("Async playback only available from system UID."); 197 } 198 mAsyncPlayer.stop(); 199 } 200 201 @Override 202 public String getTitle(Uri uri) { 203 final UserHandle user = Binder.getCallingUserHandle(); 204 return Ringtone.getTitle(getContextForUser(user), uri, 205 false /*followSettingsUri*/, false /*allowRemote*/); 206 } 207 208 @Override 209 public ParcelFileDescriptor openRingtone(Uri uri) { 210 final UserHandle user = Binder.getCallingUserHandle(); 211 final ContentResolver resolver = getContextForUser(user).getContentResolver(); 212 213 // Only open the requested Uri if it's a well-known ringtone or 214 // other sound from the platform media store, otherwise this opens 215 // up arbitrary access to any file on external storage. 216 if (uri.toString().startsWith(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) { 217 try (Cursor c = resolver.query(uri, new String[] { 218 MediaStore.Audio.AudioColumns.IS_RINGTONE, 219 MediaStore.Audio.AudioColumns.IS_ALARM, 220 MediaStore.Audio.AudioColumns.IS_NOTIFICATION 221 }, null, null, null)) { 222 if (c.moveToFirst()) { 223 if (c.getInt(0) != 0 || c.getInt(1) != 0 || c.getInt(2) != 0) { 224 try { 225 return resolver.openFileDescriptor(uri, "r"); 226 } catch (IOException e) { 227 throw new SecurityException(e); 228 } 229 } 230 } 231 } 232 } 233 throw new SecurityException("Uri is not ringtone, alarm, or notification: " + uri); 234 } 235 }; 236 getContextForUser(UserHandle user)237 private Context getContextForUser(UserHandle user) { 238 try { 239 return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user); 240 } catch (NameNotFoundException e) { 241 throw new RuntimeException(e); 242 } 243 } 244 245 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)246 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 247 pw.println("Clients:"); 248 synchronized (mClients) { 249 for (Client client : mClients.values()) { 250 pw.print(" mToken="); 251 pw.print(client.mToken); 252 pw.print(" mUri="); 253 pw.println(client.mRingtone.getUri()); 254 } 255 } 256 } 257 } 258