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             Client client;
168             synchronized (mClients) {
169                 client = mClients.get(token);
170             }
171             if (client != null) {
172                 client.mRingtone.setVolume(volume);
173                 client.mRingtone.setLooping(looping);
174             }
175             // else no client for token when setting playback properties but will be set at play()
176         }
177 
178         @Override
179         public void playAsync(Uri uri, UserHandle user, boolean looping, AudioAttributes aa) {
180             if (LOGD) Log.d(TAG, "playAsync(uri=" + uri + ", user=" + user + ")");
181             if (Binder.getCallingUid() != Process.SYSTEM_UID) {
182                 throw new SecurityException("Async playback only available from system UID.");
183             }
184             if (UserHandle.ALL.equals(user)) {
185                 user = UserHandle.SYSTEM;
186             }
187             mAsyncPlayer.play(getContextForUser(user), uri, looping, aa);
188         }
189 
190         @Override
191         public void stopAsync() {
192             if (LOGD) Log.d(TAG, "stopAsync()");
193             if (Binder.getCallingUid() != Process.SYSTEM_UID) {
194                 throw new SecurityException("Async playback only available from system UID.");
195             }
196             mAsyncPlayer.stop();
197         }
198 
199         @Override
200         public String getTitle(Uri uri) {
201             final UserHandle user = Binder.getCallingUserHandle();
202             return Ringtone.getTitle(getContextForUser(user), uri,
203                     false /*followSettingsUri*/, false /*allowRemote*/);
204         }
205 
206         @Override
207         public ParcelFileDescriptor openRingtone(Uri uri) {
208             final UserHandle user = Binder.getCallingUserHandle();
209             final ContentResolver resolver = getContextForUser(user).getContentResolver();
210 
211             // Only open the requested Uri if it's a well-known ringtone or
212             // other sound from the platform media store, otherwise this opens
213             // up arbitrary access to any file on external storage.
214             if (uri.toString().startsWith(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) {
215                 try (Cursor c = resolver.query(uri, new String[] {
216                         MediaStore.Audio.AudioColumns.IS_RINGTONE,
217                         MediaStore.Audio.AudioColumns.IS_ALARM,
218                         MediaStore.Audio.AudioColumns.IS_NOTIFICATION
219                 }, null, null, null)) {
220                     if (c.moveToFirst()) {
221                         if (c.getInt(0) != 0 || c.getInt(1) != 0 || c.getInt(2) != 0) {
222                             try {
223                                 return resolver.openFileDescriptor(uri, "r");
224                             } catch (IOException e) {
225                                 throw new SecurityException(e);
226                             }
227                         }
228                     }
229                 }
230             }
231             throw new SecurityException("Uri is not ringtone, alarm, or notification: " + uri);
232         }
233     };
234 
getContextForUser(UserHandle user)235     private Context getContextForUser(UserHandle user) {
236         try {
237             return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user);
238         } catch (NameNotFoundException e) {
239             throw new RuntimeException(e);
240         }
241     }
242 
243     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)244     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
245         pw.println("Clients:");
246         synchronized (mClients) {
247             for (Client client : mClients.values()) {
248                 pw.print("  mToken=");
249                 pw.print(client.mToken);
250                 pw.print(" mUri=");
251                 pw.println(client.mRingtone.getUri());
252             }
253         }
254     }
255 }
256