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.content.ContentResolver;
20 import android.content.Context;
21 import android.content.pm.PackageManager.NameNotFoundException;
22 import android.database.Cursor;
23 import android.media.AudioAttributes;
24 import android.media.IAudioService;
25 import android.media.IRingtonePlayer;
26 import android.media.Ringtone;
27 import android.net.Uri;
28 import android.os.Binder;
29 import android.os.IBinder;
30 import android.os.ParcelFileDescriptor;
31 import android.os.Process;
32 import android.os.RemoteException;
33 import android.os.ServiceManager;
34 import android.os.UserHandle;
35 import android.provider.MediaStore;
36 import android.provider.MediaStore.Audio.AudioColumns;
37 import android.util.Log;
38 
39 import com.android.internal.util.Preconditions;
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 
62     @Override
start()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 
Client(IBinder token, Uri uri, UserHandle user, AudioAttributes aa)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
binderDied()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 
getContextForUser(UserHandle user)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
dump(FileDescriptor fd, PrintWriter pw, String[] args)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