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.content.ContentResolver; 20 import android.content.Context; 21 import android.content.res.AssetFileDescriptor; 22 import android.content.res.Resources.NotFoundException; 23 import android.database.Cursor; 24 import android.media.MediaPlayer.OnCompletionListener; 25 import android.net.Uri; 26 import android.os.Binder; 27 import android.os.RemoteException; 28 import android.provider.MediaStore; 29 import android.provider.Settings; 30 import android.provider.MediaStore.MediaColumns; 31 import android.util.Log; 32 33 import java.io.IOException; 34 import 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 */ 45 public 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