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