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