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