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