1 /*
2  * Copyright (C) 2006 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 android.media;
18 
19 import android.content.ContentProvider;
20 import android.content.ContentResolver;
21 import android.content.Context;
22 import android.content.res.AssetFileDescriptor;
23 import android.content.res.Resources.NotFoundException;
24 import android.database.Cursor;
25 import android.media.MediaPlayer.OnCompletionListener;
26 import android.net.Uri;
27 import android.os.Binder;
28 import android.os.RemoteException;
29 import android.provider.MediaStore;
30 import android.provider.Settings;
31 import android.provider.MediaStore.MediaColumns;
32 import android.util.Log;
33 
34 import java.io.IOException;
35 import java.util.ArrayList;
36 
37 /**
38  * Ringtone provides a quick method for playing a ringtone, notification, or
39  * other similar types of sounds.
40  * <p>
41  * For ways of retrieving {@link Ringtone} objects or to show a ringtone
42  * picker, see {@link RingtoneManager}.
43  *
44  * @see RingtoneManager
45  */
46 public class Ringtone {
47     private static final String TAG = "Ringtone";
48     private static final boolean LOGD = true;
49 
50     private static final String[] MEDIA_COLUMNS = new String[] {
51         MediaStore.Audio.Media._ID,
52         MediaStore.Audio.Media.DATA,
53         MediaStore.Audio.Media.TITLE
54     };
55     /** Selection that limits query results to just audio files */
56     private static final String MEDIA_SELECTION = MediaColumns.MIME_TYPE + " LIKE 'audio/%' OR "
57             + MediaColumns.MIME_TYPE + " IN ('application/ogg', 'application/x-flac')";
58 
59     // keep references on active Ringtones until stopped or completion listener called.
60     private static final ArrayList<Ringtone> sActiveRingtones = new ArrayList<Ringtone>();
61 
62     private final Context mContext;
63     private final AudioManager mAudioManager;
64 
65     /**
66      * Flag indicating if we're allowed to fall back to remote playback using
67      * {@link #mRemotePlayer}. Typically this is false when we're the remote
68      * player and there is nobody else to delegate to.
69      */
70     private final boolean mAllowRemote;
71     private final IRingtonePlayer mRemotePlayer;
72     private final Binder mRemoteToken;
73 
74     private MediaPlayer mLocalPlayer;
75     private final MyOnCompletionListener mCompletionListener = new MyOnCompletionListener();
76 
77     private Uri mUri;
78     private String mTitle;
79 
80     private AudioAttributes mAudioAttributes = new AudioAttributes.Builder()
81             .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
82             .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
83             .build();
84     // playback properties, use synchronized with mPlaybackSettingsLock
85     private boolean mIsLooping = false;
86     private float mVolume = 1.0f;
87     private final Object mPlaybackSettingsLock = new Object();
88 
89     /** {@hide} */
Ringtone(Context context, boolean allowRemote)90     public Ringtone(Context context, boolean allowRemote) {
91         mContext = context;
92         mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
93         mAllowRemote = allowRemote;
94         mRemotePlayer = allowRemote ? mAudioManager.getRingtonePlayer() : null;
95         mRemoteToken = allowRemote ? new Binder() : null;
96     }
97 
98     /**
99      * Sets the stream type where this ringtone will be played.
100      *
101      * @param streamType The stream, see {@link AudioManager}.
102      * @deprecated use {@link #setAudioAttributes(AudioAttributes)}
103      */
104     @Deprecated
setStreamType(int streamType)105     public void setStreamType(int streamType) {
106         PlayerBase.deprecateStreamTypeForPlayback(streamType, "Ringtone", "setStreamType()");
107         setAudioAttributes(new AudioAttributes.Builder()
108                 .setInternalLegacyStreamType(streamType)
109                 .build());
110     }
111 
112     /**
113      * Gets the stream type where this ringtone will be played.
114      *
115      * @return The stream type, see {@link AudioManager}.
116      * @deprecated use of stream types is deprecated, see
117      *     {@link #setAudioAttributes(AudioAttributes)}
118      */
119     @Deprecated
getStreamType()120     public int getStreamType() {
121         return AudioAttributes.toLegacyStreamType(mAudioAttributes);
122     }
123 
124     /**
125      * Sets the {@link AudioAttributes} for this ringtone.
126      * @param attributes the non-null attributes characterizing this ringtone.
127      */
setAudioAttributes(AudioAttributes attributes)128     public void setAudioAttributes(AudioAttributes attributes)
129             throws IllegalArgumentException {
130         if (attributes == null) {
131             throw new IllegalArgumentException("Invalid null AudioAttributes for Ringtone");
132         }
133         mAudioAttributes = attributes;
134         // The audio attributes have to be set before the media player is prepared.
135         // Re-initialize it.
136         setUri(mUri);
137     }
138 
139     /**
140      * Returns the {@link AudioAttributes} used by this object.
141      * @return the {@link AudioAttributes} that were set with
142      *     {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set.
143      */
getAudioAttributes()144     public AudioAttributes getAudioAttributes() {
145         return mAudioAttributes;
146     }
147 
148     /**
149      * Sets the player to be looping or non-looping.
150      * @param looping whether to loop or not.
151      */
setLooping(boolean looping)152     public void setLooping(boolean looping) {
153         synchronized (mPlaybackSettingsLock) {
154             mIsLooping = looping;
155             applyPlaybackProperties_sync();
156         }
157     }
158 
159     /**
160      * Returns whether the looping mode was enabled on this player.
161      * @return true if this player loops when playing.
162      */
isLooping()163     public boolean isLooping() {
164         synchronized (mPlaybackSettingsLock) {
165             return mIsLooping;
166         }
167     }
168 
169     /**
170      * Sets the volume on this player.
171      * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0
172      *   corresponds to no attenuation being applied.
173      */
setVolume(float volume)174     public void setVolume(float volume) {
175         synchronized (mPlaybackSettingsLock) {
176             if (volume < 0.0f) { volume = 0.0f; }
177             if (volume > 1.0f) { volume = 1.0f; }
178             mVolume = volume;
179             applyPlaybackProperties_sync();
180         }
181     }
182 
183     /**
184      * Returns the volume scalar set on this player.
185      * @return a value between 0.0f and 1.0f.
186      */
getVolume()187     public float getVolume() {
188         synchronized (mPlaybackSettingsLock) {
189             return mVolume;
190         }
191     }
192 
193     /**
194      * Must be called synchronized on mPlaybackSettingsLock
195      */
applyPlaybackProperties_sync()196     private void applyPlaybackProperties_sync() {
197         if (mLocalPlayer != null) {
198             mLocalPlayer.setVolume(mVolume);
199             mLocalPlayer.setLooping(mIsLooping);
200         } else if (mAllowRemote && (mRemotePlayer != null)) {
201             try {
202                 mRemotePlayer.setPlaybackProperties(mRemoteToken, mVolume, mIsLooping);
203             } catch (RemoteException e) {
204                 Log.w(TAG, "Problem setting playback properties: ", e);
205             }
206         } else {
207             Log.w(TAG,
208                     "Neither local nor remote player available when applying playback properties");
209         }
210     }
211 
212     /**
213      * Returns a human-presentable title for ringtone. Looks in media
214      * content provider. If not in either, uses the filename
215      *
216      * @param context A context used for querying.
217      */
getTitle(Context context)218     public String getTitle(Context context) {
219         if (mTitle != null) return mTitle;
220         return mTitle = getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote);
221     }
222 
223     /**
224      * @hide
225      */
getTitle( Context context, Uri uri, boolean followSettingsUri, boolean allowRemote)226     public static String getTitle(
227             Context context, Uri uri, boolean followSettingsUri, boolean allowRemote) {
228         ContentResolver res = context.getContentResolver();
229 
230         String title = null;
231 
232         if (uri != null) {
233             String authority = ContentProvider.getAuthorityWithoutUserId(uri.getAuthority());
234 
235             if (Settings.AUTHORITY.equals(authority)) {
236                 if (followSettingsUri) {
237                     Uri actualUri = RingtoneManager.getActualDefaultRingtoneUri(context,
238                             RingtoneManager.getDefaultType(uri));
239                     String actualTitle = getTitle(
240                             context, actualUri, false /*followSettingsUri*/, allowRemote);
241                     title = context
242                             .getString(com.android.internal.R.string.ringtone_default_with_actual,
243                                     actualTitle);
244                 }
245             } else {
246                 Cursor cursor = null;
247                 try {
248                     if (MediaStore.AUTHORITY.equals(authority)) {
249                         final String mediaSelection = allowRemote ? null : MEDIA_SELECTION;
250                         cursor = res.query(uri, MEDIA_COLUMNS, mediaSelection, null, null);
251                         if (cursor != null && cursor.getCount() == 1) {
252                             cursor.moveToFirst();
253                             return cursor.getString(2);
254                         }
255                         // missing cursor is handled below
256                     }
257                 } catch (SecurityException e) {
258                     IRingtonePlayer mRemotePlayer = null;
259                     if (allowRemote) {
260                         AudioManager audioManager =
261                                 (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
262                         mRemotePlayer = audioManager.getRingtonePlayer();
263                     }
264                     if (mRemotePlayer != null) {
265                         try {
266                             title = mRemotePlayer.getTitle(uri);
267                         } catch (RemoteException re) {
268                         }
269                     }
270                 } finally {
271                     if (cursor != null) {
272                         cursor.close();
273                     }
274                     cursor = null;
275                 }
276                 if (title == null) {
277                     title = uri.getLastPathSegment();
278                 }
279             }
280         } else {
281             title = context.getString(com.android.internal.R.string.ringtone_silent);
282         }
283 
284         if (title == null) {
285             title = context.getString(com.android.internal.R.string.ringtone_unknown);
286             if (title == null) {
287                 title = "";
288             }
289         }
290 
291         return title;
292     }
293 
294     /**
295      * Set {@link Uri} to be used for ringtone playback. Attempts to open
296      * locally, otherwise will delegate playback to remote
297      * {@link IRingtonePlayer}.
298      *
299      * @hide
300      */
setUri(Uri uri)301     public void setUri(Uri uri) {
302         destroyLocalPlayer();
303 
304         mUri = uri;
305         if (mUri == null) {
306             return;
307         }
308 
309         // TODO: detect READ_EXTERNAL and specific content provider case, instead of relying on throwing
310 
311         // try opening uri locally before delegating to remote player
312         mLocalPlayer = new MediaPlayer();
313         try {
314             mLocalPlayer.setDataSource(mContext, mUri);
315             mLocalPlayer.setAudioAttributes(mAudioAttributes);
316             synchronized (mPlaybackSettingsLock) {
317                 applyPlaybackProperties_sync();
318             }
319             mLocalPlayer.prepare();
320 
321         } catch (SecurityException | IOException e) {
322             destroyLocalPlayer();
323             if (!mAllowRemote) {
324                 Log.w(TAG, "Remote playback not allowed: " + e);
325             }
326         }
327 
328         if (LOGD) {
329             if (mLocalPlayer != null) {
330                 Log.d(TAG, "Successfully created local player");
331             } else {
332                 Log.d(TAG, "Problem opening; delegating to remote player");
333             }
334         }
335     }
336 
337     /** {@hide} */
getUri()338     public Uri getUri() {
339         return mUri;
340     }
341 
342     /**
343      * Plays the ringtone.
344      */
play()345     public void play() {
346         if (mLocalPlayer != null) {
347             // do not play ringtones if stream volume is 0
348             // (typically because ringer mode is silent).
349             if (mAudioManager.getStreamVolume(
350                     AudioAttributes.toLegacyStreamType(mAudioAttributes)) != 0) {
351                 startLocalPlayer();
352             }
353         } else if (mAllowRemote && (mRemotePlayer != null)) {
354             final Uri canonicalUri = mUri.getCanonicalUri();
355             final boolean looping;
356             final float volume;
357             synchronized (mPlaybackSettingsLock) {
358                 looping = mIsLooping;
359                 volume = mVolume;
360             }
361             try {
362                 mRemotePlayer.play(mRemoteToken, canonicalUri, mAudioAttributes, volume, looping);
363             } catch (RemoteException e) {
364                 if (!playFallbackRingtone()) {
365                     Log.w(TAG, "Problem playing ringtone: " + e);
366                 }
367             }
368         } else {
369             if (!playFallbackRingtone()) {
370                 Log.w(TAG, "Neither local nor remote playback available");
371             }
372         }
373     }
374 
375     /**
376      * Stops a playing ringtone.
377      */
stop()378     public void stop() {
379         if (mLocalPlayer != null) {
380             destroyLocalPlayer();
381         } else if (mAllowRemote && (mRemotePlayer != null)) {
382             try {
383                 mRemotePlayer.stop(mRemoteToken);
384             } catch (RemoteException e) {
385                 Log.w(TAG, "Problem stopping ringtone: " + e);
386             }
387         }
388     }
389 
destroyLocalPlayer()390     private void destroyLocalPlayer() {
391         if (mLocalPlayer != null) {
392             mLocalPlayer.setOnCompletionListener(null);
393             mLocalPlayer.reset();
394             mLocalPlayer.release();
395             mLocalPlayer = null;
396             synchronized (sActiveRingtones) {
397                 sActiveRingtones.remove(this);
398             }
399         }
400     }
401 
startLocalPlayer()402     private void startLocalPlayer() {
403         if (mLocalPlayer == null) {
404             return;
405         }
406         synchronized (sActiveRingtones) {
407             sActiveRingtones.add(this);
408         }
409         mLocalPlayer.setOnCompletionListener(mCompletionListener);
410         mLocalPlayer.start();
411     }
412 
413     /**
414      * Whether this ringtone is currently playing.
415      *
416      * @return True if playing, false otherwise.
417      */
isPlaying()418     public boolean isPlaying() {
419         if (mLocalPlayer != null) {
420             return mLocalPlayer.isPlaying();
421         } else if (mAllowRemote && (mRemotePlayer != null)) {
422             try {
423                 return mRemotePlayer.isPlaying(mRemoteToken);
424             } catch (RemoteException e) {
425                 Log.w(TAG, "Problem checking ringtone: " + e);
426                 return false;
427             }
428         } else {
429             Log.w(TAG, "Neither local nor remote playback available");
430             return false;
431         }
432     }
433 
playFallbackRingtone()434     private boolean playFallbackRingtone() {
435         if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes))
436                 != 0) {
437             int ringtoneType = RingtoneManager.getDefaultType(mUri);
438             if (ringtoneType == -1 ||
439                     RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) != null) {
440                 // Default ringtone, try fallback ringtone.
441                 try {
442                     AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(
443                             com.android.internal.R.raw.fallbackring);
444                     if (afd != null) {
445                         mLocalPlayer = new MediaPlayer();
446                         if (afd.getDeclaredLength() < 0) {
447                             mLocalPlayer.setDataSource(afd.getFileDescriptor());
448                         } else {
449                             mLocalPlayer.setDataSource(afd.getFileDescriptor(),
450                                     afd.getStartOffset(),
451                                     afd.getDeclaredLength());
452                         }
453                         mLocalPlayer.setAudioAttributes(mAudioAttributes);
454                         synchronized (mPlaybackSettingsLock) {
455                             applyPlaybackProperties_sync();
456                         }
457                         mLocalPlayer.prepare();
458                         startLocalPlayer();
459                         afd.close();
460                         return true;
461                     } else {
462                         Log.e(TAG, "Could not load fallback ringtone");
463                     }
464                 } catch (IOException ioe) {
465                     destroyLocalPlayer();
466                     Log.e(TAG, "Failed to open fallback ringtone");
467                 } catch (NotFoundException nfe) {
468                     Log.e(TAG, "Fallback ringtone does not exist");
469                 }
470             } else {
471                 Log.w(TAG, "not playing fallback for " + mUri);
472             }
473         }
474         return false;
475     }
476 
setTitle(String title)477     void setTitle(String title) {
478         mTitle = title;
479     }
480 
481     @Override
finalize()482     protected void finalize() {
483         if (mLocalPlayer != null) {
484             mLocalPlayer.release();
485         }
486     }
487 
488     class MyOnCompletionListener implements MediaPlayer.OnCompletionListener {
489         @Override
onCompletion(MediaPlayer mp)490         public void onCompletion(MediaPlayer mp) {
491             synchronized (sActiveRingtones) {
492                 sActiveRingtones.remove(Ringtone.this);
493             }
494             mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle.
495         }
496     }
497 }
498