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