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