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