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