• 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      * @return true if media player creation succeeded or is deferred,
194      * false if it did not succeed and can't be tried remotely.
195      * @hide
196      */
createLocalMediaPlayer()197     public boolean createLocalMediaPlayer() {
198         Trace.beginSection("createLocalMediaPlayer");
199         if (mUri == null) {
200             Log.e(TAG, "Could not create media player as no URI was provided.");
201             return mAllowRemote && mRemotePlayer != null;
202         }
203         destroyLocalPlayer();
204         // try opening uri locally before delegating to remote player
205         mLocalPlayer = new MediaPlayer();
206         try {
207             mLocalPlayer.setDataSource(mContext, mUri);
208             mLocalPlayer.setAudioAttributes(mAudioAttributes);
209             mLocalPlayer.setPreferredDevice(
210                     mPreferBuiltinDevice ? getBuiltinDevice(mAudioManager) : null);
211             synchronized (mPlaybackSettingsLock) {
212                 applyPlaybackProperties_sync();
213             }
214             if (mVolumeShaperConfig != null) {
215                 mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig);
216             }
217             mLocalPlayer.prepare();
218 
219         } catch (SecurityException | IOException e) {
220             destroyLocalPlayer();
221             if (!mAllowRemote) {
222                 Log.w(TAG, "Remote playback not allowed: " + e);
223             }
224         }
225 
226         if (LOGD) {
227             if (mLocalPlayer != null) {
228                 Log.d(TAG, "Successfully created local player");
229             } else {
230                 Log.d(TAG, "Problem opening; delegating to remote player");
231             }
232         }
233         Trace.endSection();
234         return mLocalPlayer != null || (mAllowRemote && mRemotePlayer != null);
235     }
236 
237     /**
238      * Same as AudioManager.hasHapticChannels except it assumes an already created ringtone.
239      * If the ringtone has not been created, it will load based on URI provided at {@link #setUri}
240      * and if not URI has been set, it will assume no haptic channels are present.
241      * @hide
242      */
hasHapticChannels()243     public boolean hasHapticChannels() {
244         // FIXME: support remote player, or internalize haptic channels support and remove entirely.
245         try {
246             android.os.Trace.beginSection("Ringtone.hasHapticChannels");
247             if (mLocalPlayer != null) {
248                 for(MediaPlayer.TrackInfo trackInfo : mLocalPlayer.getTrackInfo()) {
249                     if (trackInfo.hasHapticChannels()) {
250                         return true;
251                     }
252                 }
253             }
254         } finally {
255             android.os.Trace.endSection();
256         }
257         return false;
258     }
259 
260     /**
261      * Returns whether a local player has been created for this ringtone.
262      * @hide
263      */
264     @VisibleForTesting
hasLocalPlayer()265     public boolean hasLocalPlayer() {
266         return mLocalPlayer != null;
267     }
268 
269     /**
270      * Returns the {@link AudioAttributes} used by this object.
271      * @return the {@link AudioAttributes} that were set with
272      *     {@link #setAudioAttributes(AudioAttributes)} or the default attributes if none were set.
273      */
getAudioAttributes()274     public AudioAttributes getAudioAttributes() {
275         return mAudioAttributes;
276     }
277 
278     /**
279      * Sets the player to be looping or non-looping.
280      * @param looping whether to loop or not.
281      */
setLooping(boolean looping)282     public void setLooping(boolean looping) {
283         synchronized (mPlaybackSettingsLock) {
284             mIsLooping = looping;
285             applyPlaybackProperties_sync();
286         }
287     }
288 
289     /**
290      * Returns whether the looping mode was enabled on this player.
291      * @return true if this player loops when playing.
292      */
isLooping()293     public boolean isLooping() {
294         synchronized (mPlaybackSettingsLock) {
295             return mIsLooping;
296         }
297     }
298 
299     /**
300      * Sets the volume on this player.
301      * @param volume a raw scalar in range 0.0 to 1.0, where 0.0 mutes this player, and 1.0
302      *   corresponds to no attenuation being applied.
303      */
setVolume(float volume)304     public void setVolume(float volume) {
305         synchronized (mPlaybackSettingsLock) {
306             if (volume < 0.0f) { volume = 0.0f; }
307             if (volume > 1.0f) { volume = 1.0f; }
308             mVolume = volume;
309             applyPlaybackProperties_sync();
310         }
311     }
312 
313     /**
314      * Returns the volume scalar set on this player.
315      * @return a value between 0.0f and 1.0f.
316      */
getVolume()317     public float getVolume() {
318         synchronized (mPlaybackSettingsLock) {
319             return mVolume;
320         }
321     }
322 
323     /**
324      * Enable or disable the {@link android.media.audiofx.HapticGenerator} effect. The effect can
325      * only be enabled on devices that support the effect.
326      *
327      * @return true if the HapticGenerator effect is successfully enabled. Otherwise, return false.
328      * @see android.media.audiofx.HapticGenerator#isAvailable()
329      */
setHapticGeneratorEnabled(boolean enabled)330     public boolean setHapticGeneratorEnabled(boolean enabled) {
331         if (!HapticGenerator.isAvailable()) {
332             return false;
333         }
334         synchronized (mPlaybackSettingsLock) {
335             mHapticGeneratorEnabled = enabled;
336             applyPlaybackProperties_sync();
337         }
338         return true;
339     }
340 
341     /**
342      * Return whether the {@link android.media.audiofx.HapticGenerator} effect is enabled or not.
343      * @return true if the HapticGenerator is enabled.
344      */
isHapticGeneratorEnabled()345     public boolean isHapticGeneratorEnabled() {
346         synchronized (mPlaybackSettingsLock) {
347             return mHapticGeneratorEnabled;
348         }
349     }
350 
351     /**
352      * Must be called synchronized on mPlaybackSettingsLock
353      */
applyPlaybackProperties_sync()354     private void applyPlaybackProperties_sync() {
355         if (mLocalPlayer != null) {
356             mLocalPlayer.setVolume(mVolume);
357             mLocalPlayer.setLooping(mIsLooping);
358             if (mHapticGenerator == null && mHapticGeneratorEnabled) {
359                 mHapticGenerator = HapticGenerator.create(mLocalPlayer.getAudioSessionId());
360             }
361             if (mHapticGenerator != null) {
362                 mHapticGenerator.setEnabled(mHapticGeneratorEnabled);
363             }
364         } else if (mAllowRemote && (mRemotePlayer != null)) {
365             try {
366                 mRemotePlayer.setPlaybackProperties(
367                         mRemoteToken, mVolume, mIsLooping, mHapticGeneratorEnabled);
368             } catch (RemoteException e) {
369                 Log.w(TAG, "Problem setting playback properties: ", e);
370             }
371         } else {
372             Log.w(TAG,
373                     "Neither local nor remote player available when applying playback properties");
374         }
375     }
376 
377     /**
378      * Returns a human-presentable title for ringtone. Looks in media
379      * content provider. If not in either, uses the filename
380      *
381      * @param context A context used for querying.
382      */
getTitle(Context context)383     public String getTitle(Context context) {
384         if (mTitle != null) return mTitle;
385         return mTitle = getTitle(context, mUri, true /*followSettingsUri*/, mAllowRemote);
386     }
387 
388     /**
389      * @hide
390      */
getTitle( Context context, Uri uri, boolean followSettingsUri, boolean allowRemote)391     public static String getTitle(
392             Context context, Uri uri, boolean followSettingsUri, boolean allowRemote) {
393         ContentResolver res = context.getContentResolver();
394 
395         String title = null;
396 
397         if (uri != null) {
398             String authority = ContentProvider.getAuthorityWithoutUserId(uri.getAuthority());
399 
400             if (Settings.AUTHORITY.equals(authority)) {
401                 if (followSettingsUri) {
402                     Uri actualUri = RingtoneManager.getActualDefaultRingtoneUri(context,
403                             RingtoneManager.getDefaultType(uri));
404                     String actualTitle = getTitle(
405                             context, actualUri, false /*followSettingsUri*/, allowRemote);
406                     title = context
407                             .getString(com.android.internal.R.string.ringtone_default_with_actual,
408                                     actualTitle);
409                 }
410             } else {
411                 Cursor cursor = null;
412                 try {
413                     if (MediaStore.AUTHORITY.equals(authority)) {
414                         final String mediaSelection = allowRemote ? null : MEDIA_SELECTION;
415                         cursor = res.query(uri, MEDIA_COLUMNS, mediaSelection, null, null);
416                         if (cursor != null && cursor.getCount() == 1) {
417                             cursor.moveToFirst();
418                             return cursor.getString(1);
419                         }
420                         // missing cursor is handled below
421                     }
422                 } catch (SecurityException e) {
423                     IRingtonePlayer mRemotePlayer = null;
424                     if (allowRemote) {
425                         AudioManager audioManager =
426                                 (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
427                         mRemotePlayer = audioManager.getRingtonePlayer();
428                     }
429                     if (mRemotePlayer != null) {
430                         try {
431                             title = mRemotePlayer.getTitle(uri);
432                         } catch (RemoteException re) {
433                         }
434                     }
435                 } finally {
436                     if (cursor != null) {
437                         cursor.close();
438                     }
439                     cursor = null;
440                 }
441                 if (title == null) {
442                     title = uri.getLastPathSegment();
443                 }
444             }
445         } else {
446             title = context.getString(com.android.internal.R.string.ringtone_silent);
447         }
448 
449         if (title == null) {
450             title = context.getString(com.android.internal.R.string.ringtone_unknown);
451             if (title == null) {
452                 title = "";
453             }
454         }
455 
456         return title;
457     }
458 
459     /**
460      * Set {@link Uri} to be used for ringtone playback.
461      * {@link IRingtonePlayer}.
462      *
463      * @hide
464      */
465     @UnsupportedAppUsage
setUri(Uri uri)466     public void setUri(Uri uri) {
467         setUri(uri, null);
468     }
469 
470     /**
471      * @hide
472      */
setVolumeShaperConfig(@ullable VolumeShaper.Configuration volumeShaperConfig)473     public void setVolumeShaperConfig(@Nullable VolumeShaper.Configuration volumeShaperConfig) {
474         mVolumeShaperConfig = volumeShaperConfig;
475     }
476 
477     /**
478      * Set {@link Uri} to be used for ringtone playback. Attempts to open
479      * locally, otherwise will delegate playback to remote
480      * {@link IRingtonePlayer}. Add {@link VolumeShaper} if required.
481      *
482      * @hide
483      */
setUri(Uri uri, @Nullable VolumeShaper.Configuration volumeShaperConfig)484     public void setUri(Uri uri, @Nullable VolumeShaper.Configuration volumeShaperConfig) {
485         mVolumeShaperConfig = volumeShaperConfig;
486         mUri = uri;
487         if (mUri == null) {
488             destroyLocalPlayer();
489         }
490     }
491 
492     /** {@hide} */
493     @UnsupportedAppUsage
getUri()494     public Uri getUri() {
495         return mUri;
496     }
497 
498     /**
499      * Plays the ringtone.
500      */
play()501     public void play() {
502         if (mLocalPlayer != null) {
503             // Play ringtones if stream volume is over 0 or if it is a haptic-only ringtone
504             // (typically because ringer mode is vibrate).
505             if (mAudioManager.getStreamVolume(AudioAttributes.toLegacyStreamType(mAudioAttributes))
506                     != 0) {
507                 startLocalPlayer();
508             } else if (!mAudioAttributes.areHapticChannelsMuted() && hasHapticChannels()) {
509                 // is haptic only ringtone
510                 startLocalPlayer();
511             }
512         } else if (mAllowRemote && (mRemotePlayer != null) && (mUri != null)) {
513             final Uri canonicalUri = mUri.getCanonicalUri();
514             final boolean looping;
515             final float volume;
516             synchronized (mPlaybackSettingsLock) {
517                 looping = mIsLooping;
518                 volume = mVolume;
519             }
520             try {
521                 mRemotePlayer.playWithVolumeShaping(mRemoteToken, canonicalUri, mAudioAttributes,
522                         volume, looping, mVolumeShaperConfig);
523             } catch (RemoteException e) {
524                 if (!playFallbackRingtone()) {
525                     Log.w(TAG, "Problem playing ringtone: " + e);
526                 }
527             }
528         } else {
529             if (!playFallbackRingtone()) {
530                 Log.w(TAG, "Neither local nor remote playback available");
531             }
532         }
533     }
534 
535     /**
536      * Stops a playing ringtone.
537      */
stop()538     public void stop() {
539         if (mLocalPlayer != null) {
540             destroyLocalPlayer();
541         } else if (mAllowRemote && (mRemotePlayer != null)) {
542             try {
543                 mRemotePlayer.stop(mRemoteToken);
544             } catch (RemoteException e) {
545                 Log.w(TAG, "Problem stopping ringtone: " + e);
546             }
547         }
548     }
549 
destroyLocalPlayer()550     private void destroyLocalPlayer() {
551         if (mLocalPlayer != null) {
552             if (mHapticGenerator != null) {
553                 mHapticGenerator.release();
554                 mHapticGenerator = null;
555             }
556             mLocalPlayer.setOnCompletionListener(null);
557             mLocalPlayer.reset();
558             mLocalPlayer.release();
559             mLocalPlayer = null;
560             mVolumeShaper = null;
561             synchronized (sActiveRingtones) {
562                 sActiveRingtones.remove(this);
563             }
564         }
565     }
566 
startLocalPlayer()567     private void startLocalPlayer() {
568         if (mLocalPlayer == null) {
569             return;
570         }
571         synchronized (sActiveRingtones) {
572             sActiveRingtones.add(this);
573         }
574         mLocalPlayer.setOnCompletionListener(mCompletionListener);
575         mLocalPlayer.start();
576         if (mVolumeShaper != null) {
577             mVolumeShaper.apply(VolumeShaper.Operation.PLAY);
578         }
579     }
580 
581     /**
582      * Whether this ringtone is currently playing.
583      *
584      * @return True if playing, false otherwise.
585      */
isPlaying()586     public boolean isPlaying() {
587         if (mLocalPlayer != null) {
588             return mLocalPlayer.isPlaying();
589         } else if (mAllowRemote && (mRemotePlayer != null)) {
590             try {
591                 return mRemotePlayer.isPlaying(mRemoteToken);
592             } catch (RemoteException e) {
593                 Log.w(TAG, "Problem checking ringtone: " + e);
594                 return false;
595             }
596         } else {
597             Log.w(TAG, "Neither local nor remote playback available");
598             return false;
599         }
600     }
601 
playFallbackRingtone()602     private boolean playFallbackRingtone() {
603         int streamType = AudioAttributes.toLegacyStreamType(mAudioAttributes);
604         if (mAudioManager.getStreamVolume(streamType) == 0) {
605             return false;
606         }
607         int ringtoneType = RingtoneManager.getDefaultType(mUri);
608         if (ringtoneType != -1 &&
609                 RingtoneManager.getActualDefaultRingtoneUri(mContext, ringtoneType) == null) {
610             Log.w(TAG, "not playing fallback for " + mUri);
611             return false;
612         }
613         // Default ringtone, try fallback ringtone.
614         try {
615             AssetFileDescriptor afd = mContext.getResources().openRawResourceFd(
616                     com.android.internal.R.raw.fallbackring);
617             if (afd == null) {
618                 Log.e(TAG, "Could not load fallback ringtone");
619                 return false;
620             }
621             mLocalPlayer = new MediaPlayer();
622             if (afd.getDeclaredLength() < 0) {
623                 mLocalPlayer.setDataSource(afd.getFileDescriptor());
624             } else {
625                 mLocalPlayer.setDataSource(afd.getFileDescriptor(),
626                         afd.getStartOffset(),
627                         afd.getDeclaredLength());
628             }
629             mLocalPlayer.setAudioAttributes(mAudioAttributes);
630             synchronized (mPlaybackSettingsLock) {
631                 applyPlaybackProperties_sync();
632             }
633             if (mVolumeShaperConfig != null) {
634                 mVolumeShaper = mLocalPlayer.createVolumeShaper(mVolumeShaperConfig);
635             }
636             mLocalPlayer.prepare();
637             startLocalPlayer();
638             afd.close();
639         } catch (IOException ioe) {
640             destroyLocalPlayer();
641             Log.e(TAG, "Failed to open fallback ringtone");
642             return false;
643         } catch (NotFoundException nfe) {
644             Log.e(TAG, "Fallback ringtone does not exist");
645             return false;
646         }
647         return true;
648     }
649 
setTitle(String title)650     void setTitle(String title) {
651         mTitle = title;
652     }
653 
654     @Override
finalize()655     protected void finalize() {
656         if (mLocalPlayer != null) {
657             mLocalPlayer.release();
658         }
659     }
660 
661     class MyOnCompletionListener implements MediaPlayer.OnCompletionListener {
662         @Override
onCompletion(MediaPlayer mp)663         public void onCompletion(MediaPlayer mp) {
664             synchronized (sActiveRingtones) {
665                 sActiveRingtones.remove(Ringtone.this);
666             }
667             mp.setOnCompletionListener(null); // Help the Java GC: break the refcount cycle.
668         }
669     }
670 }
671