• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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