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