1 /* 2 * Copyright (C) 2007 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 com.android.providers.media; 18 19 import android.content.DialogInterface; 20 import android.content.Intent; 21 import android.content.res.Resources; 22 import android.content.res.Resources.NotFoundException; 23 import android.database.Cursor; 24 import android.database.CursorWrapper; 25 import android.media.AudioAttributes; 26 import android.media.Ringtone; 27 import android.media.RingtoneManager; 28 import android.net.Uri; 29 import android.os.Bundle; 30 import android.os.Handler; 31 import android.provider.MediaStore; 32 import android.provider.Settings; 33 import android.util.Log; 34 import android.util.TypedValue; 35 import android.view.View; 36 import android.widget.AdapterView; 37 import android.widget.ListView; 38 import android.widget.TextView; 39 40 import com.android.internal.app.AlertActivity; 41 import com.android.internal.app.AlertController; 42 43 import java.util.regex.Pattern; 44 45 /** 46 * The {@link RingtonePickerActivity} allows the user to choose one from all of the 47 * available ringtones. The chosen ringtone's URI will be persisted as a string. 48 * 49 * @see RingtoneManager#ACTION_RINGTONE_PICKER 50 */ 51 public final class RingtonePickerActivity extends AlertActivity implements 52 AdapterView.OnItemSelectedListener, Runnable, DialogInterface.OnClickListener, 53 AlertController.AlertParams.OnPrepareListViewListener { 54 55 private static final int POS_UNKNOWN = -1; 56 57 private static final String TAG = "RingtonePickerActivity"; 58 59 private static final int DELAY_MS_SELECTION_PLAYED = 300; 60 61 private static final String COLUMN_LABEL = MediaStore.Audio.Media.TITLE; 62 63 private static final String SAVE_CLICKED_POS = "clicked_pos"; 64 65 private static final String SOUND_NAME_RES_PREFIX = "sound_name_"; 66 67 private RingtoneManager mRingtoneManager; 68 private int mType; 69 70 private Cursor mCursor; 71 private Handler mHandler; 72 73 /** The position in the list of the 'Silent' item. */ 74 private int mSilentPos = POS_UNKNOWN; 75 76 /** The position in the list of the 'Default' item. */ 77 private int mDefaultRingtonePos = POS_UNKNOWN; 78 79 /** The position in the list of the last clicked item. */ 80 private int mClickedPos = POS_UNKNOWN; 81 82 /** The position in the list of the ringtone to sample. */ 83 private int mSampleRingtonePos = POS_UNKNOWN; 84 85 /** Whether this list has the 'Silent' item. */ 86 private boolean mHasSilentItem; 87 88 /** The Uri to place a checkmark next to. */ 89 private Uri mExistingUri; 90 91 /** The number of static items in the list. */ 92 private int mStaticItemCount; 93 94 /** Whether this list has the 'Default' item. */ 95 private boolean mHasDefaultItem; 96 97 /** The Uri to play when the 'Default' item is clicked. */ 98 private Uri mUriForDefaultItem; 99 100 /** 101 * A Ringtone for the default ringtone. In most cases, the RingtoneManager 102 * will stop the previous ringtone. However, the RingtoneManager doesn't 103 * manage the default ringtone for us, so we should stop this one manually. 104 */ 105 private Ringtone mDefaultRingtone; 106 107 /** 108 * The ringtone that's currently playing, unless the currently playing one is the default 109 * ringtone. 110 */ 111 private Ringtone mCurrentRingtone; 112 113 private int mAttributesFlags; 114 115 /** 116 * Keep the currently playing ringtone around when changing orientation, so that it 117 * can be stopped later, after the activity is recreated. 118 */ 119 private static Ringtone sPlayingRingtone; 120 121 private DialogInterface.OnClickListener mRingtoneClickListener = 122 new DialogInterface.OnClickListener() { 123 124 /* 125 * On item clicked 126 */ 127 public void onClick(DialogInterface dialog, int which) { 128 // Save the position of most recently clicked item 129 mClickedPos = which; 130 131 // Play clip 132 playRingtone(which, 0); 133 } 134 135 }; 136 137 @Override onCreate(Bundle savedInstanceState)138 protected void onCreate(Bundle savedInstanceState) { 139 super.onCreate(savedInstanceState); 140 141 mHandler = new Handler(); 142 143 Intent intent = getIntent(); 144 145 /* 146 * Get whether to show the 'Default' item, and the URI to play when the 147 * default is clicked 148 */ 149 mHasDefaultItem = intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); 150 mUriForDefaultItem = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI); 151 if (mUriForDefaultItem == null) { 152 mUriForDefaultItem = Settings.System.DEFAULT_RINGTONE_URI; 153 } 154 155 if (savedInstanceState != null) { 156 mClickedPos = savedInstanceState.getInt(SAVE_CLICKED_POS, POS_UNKNOWN); 157 } 158 // Get whether to show the 'Silent' item 159 mHasSilentItem = intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); 160 // AudioAttributes flags 161 mAttributesFlags |= intent.getIntExtra( 162 RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS, 163 0 /*defaultValue == no flags*/); 164 165 // Give the Activity so it can do managed queries 166 mRingtoneManager = new RingtoneManager(this); 167 168 // Get the types of ringtones to show 169 mType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, -1); 170 if (mType != -1) { 171 mRingtoneManager.setType(mType); 172 } 173 174 mCursor = new LocalizedCursor(mRingtoneManager.getCursor(), getResources(), COLUMN_LABEL); 175 176 // The volume keys will control the stream that we are choosing a ringtone for 177 setVolumeControlStream(mRingtoneManager.inferStreamType()); 178 179 // Get the URI whose list item should have a checkmark 180 mExistingUri = intent 181 .getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI); 182 183 final AlertController.AlertParams p = mAlertParams; 184 p.mCursor = mCursor; 185 p.mOnClickListener = mRingtoneClickListener; 186 p.mLabelColumn = COLUMN_LABEL; 187 p.mIsSingleChoice = true; 188 p.mOnItemSelectedListener = this; 189 p.mPositiveButtonText = getString(com.android.internal.R.string.ok); 190 p.mPositiveButtonListener = this; 191 p.mNegativeButtonText = getString(com.android.internal.R.string.cancel); 192 p.mPositiveButtonListener = this; 193 p.mOnPrepareListViewListener = this; 194 195 p.mTitle = intent.getCharSequenceExtra(RingtoneManager.EXTRA_RINGTONE_TITLE); 196 if (p.mTitle == null) { 197 p.mTitle = getString(com.android.internal.R.string.ringtone_picker_title); 198 } 199 200 setupAlert(); 201 } 202 @Override onSaveInstanceState(Bundle outState)203 public void onSaveInstanceState(Bundle outState) { 204 super.onSaveInstanceState(outState); 205 outState.putInt(SAVE_CLICKED_POS, mClickedPos); 206 } 207 onPrepareListView(ListView listView)208 public void onPrepareListView(ListView listView) { 209 210 if (mHasDefaultItem) { 211 mDefaultRingtonePos = addDefaultRingtoneItem(listView); 212 213 if (mClickedPos == POS_UNKNOWN && RingtoneManager.isDefault(mExistingUri)) { 214 mClickedPos = mDefaultRingtonePos; 215 } 216 } 217 218 if (mHasSilentItem) { 219 mSilentPos = addSilentItem(listView); 220 221 // The 'Silent' item should use a null Uri 222 if (mClickedPos == POS_UNKNOWN && mExistingUri == null) { 223 mClickedPos = mSilentPos; 224 } 225 } 226 227 if (mClickedPos == POS_UNKNOWN) { 228 mClickedPos = getListPosition(mRingtoneManager.getRingtonePosition(mExistingUri)); 229 } 230 231 // Put a checkmark next to an item. 232 mAlertParams.mCheckedItem = mClickedPos; 233 } 234 235 /** 236 * Adds a static item to the top of the list. A static item is one that is not from the 237 * RingtoneManager. 238 * 239 * @param listView The ListView to add to. 240 * @param textResId The resource ID of the text for the item. 241 * @return The position of the inserted item. 242 */ addStaticItem(ListView listView, int textResId)243 private int addStaticItem(ListView listView, int textResId) { 244 TextView textView = (TextView) getLayoutInflater().inflate( 245 com.android.internal.R.layout.select_dialog_singlechoice_material, listView, false); 246 textView.setText(textResId); 247 listView.addHeaderView(textView); 248 mStaticItemCount++; 249 return listView.getHeaderViewsCount() - 1; 250 } 251 addDefaultRingtoneItem(ListView listView)252 private int addDefaultRingtoneItem(ListView listView) { 253 if (mType == RingtoneManager.TYPE_NOTIFICATION) { 254 return addStaticItem(listView, R.string.notification_sound_default); 255 } else if (mType == RingtoneManager.TYPE_ALARM) { 256 return addStaticItem(listView, R.string.alarm_sound_default); 257 } 258 259 return addStaticItem(listView, R.string.ringtone_default); 260 } 261 addSilentItem(ListView listView)262 private int addSilentItem(ListView listView) { 263 return addStaticItem(listView, com.android.internal.R.string.ringtone_silent); 264 } 265 266 /* 267 * On click of Ok/Cancel buttons 268 */ onClick(DialogInterface dialog, int which)269 public void onClick(DialogInterface dialog, int which) { 270 boolean positiveResult = which == DialogInterface.BUTTON_POSITIVE; 271 272 // Stop playing the previous ringtone 273 mRingtoneManager.stopPreviousRingtone(); 274 275 if (positiveResult) { 276 Intent resultIntent = new Intent(); 277 Uri uri = null; 278 279 if (mClickedPos == mDefaultRingtonePos) { 280 // Set it to the default Uri that they originally gave us 281 uri = mUriForDefaultItem; 282 } else if (mClickedPos == mSilentPos) { 283 // A null Uri is for the 'Silent' item 284 uri = null; 285 } else { 286 uri = mRingtoneManager.getRingtoneUri(getRingtoneManagerPosition(mClickedPos)); 287 } 288 289 resultIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, uri); 290 setResult(RESULT_OK, resultIntent); 291 } else { 292 setResult(RESULT_CANCELED); 293 } 294 295 finish(); 296 } 297 298 /* 299 * On item selected via keys 300 */ onItemSelected(AdapterView parent, View view, int position, long id)301 public void onItemSelected(AdapterView parent, View view, int position, long id) { 302 playRingtone(position, DELAY_MS_SELECTION_PLAYED); 303 } 304 onNothingSelected(AdapterView parent)305 public void onNothingSelected(AdapterView parent) { 306 } 307 playRingtone(int position, int delayMs)308 private void playRingtone(int position, int delayMs) { 309 mHandler.removeCallbacks(this); 310 mSampleRingtonePos = position; 311 mHandler.postDelayed(this, delayMs); 312 } 313 run()314 public void run() { 315 stopAnyPlayingRingtone(); 316 if (mSampleRingtonePos == mSilentPos) { 317 return; 318 } 319 320 Ringtone ringtone; 321 if (mSampleRingtonePos == mDefaultRingtonePos) { 322 if (mDefaultRingtone == null) { 323 mDefaultRingtone = RingtoneManager.getRingtone(this, mUriForDefaultItem); 324 } 325 /* 326 * Stream type of mDefaultRingtone is not set explicitly here. 327 * It should be set in accordance with mRingtoneManager of this Activity. 328 */ 329 if (mDefaultRingtone != null) { 330 mDefaultRingtone.setStreamType(mRingtoneManager.inferStreamType()); 331 } 332 ringtone = mDefaultRingtone; 333 mCurrentRingtone = null; 334 } else { 335 ringtone = mRingtoneManager.getRingtone(getRingtoneManagerPosition(mSampleRingtonePos)); 336 mCurrentRingtone = ringtone; 337 } 338 339 if (ringtone != null) { 340 if (mAttributesFlags != 0) { 341 ringtone.setAudioAttributes( 342 new AudioAttributes.Builder(ringtone.getAudioAttributes()) 343 .setFlags(mAttributesFlags) 344 .build()); 345 } 346 ringtone.play(); 347 } 348 } 349 350 @Override onStop()351 protected void onStop() { 352 super.onStop(); 353 mCursor.deactivate(); 354 355 if (!isChangingConfigurations()) { 356 stopAnyPlayingRingtone(); 357 } else { 358 saveAnyPlayingRingtone(); 359 } 360 } 361 362 @Override onPause()363 protected void onPause() { 364 super.onPause(); 365 if (!isChangingConfigurations()) { 366 stopAnyPlayingRingtone(); 367 } 368 } 369 saveAnyPlayingRingtone()370 private void saveAnyPlayingRingtone() { 371 if (mDefaultRingtone != null && mDefaultRingtone.isPlaying()) { 372 sPlayingRingtone = mDefaultRingtone; 373 } else if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) { 374 sPlayingRingtone = mCurrentRingtone; 375 } 376 } 377 stopAnyPlayingRingtone()378 private void stopAnyPlayingRingtone() { 379 if (sPlayingRingtone != null && sPlayingRingtone.isPlaying()) { 380 sPlayingRingtone.stop(); 381 } 382 sPlayingRingtone = null; 383 384 if (mDefaultRingtone != null && mDefaultRingtone.isPlaying()) { 385 mDefaultRingtone.stop(); 386 } 387 388 if (mRingtoneManager != null) { 389 mRingtoneManager.stopPreviousRingtone(); 390 } 391 } 392 getRingtoneManagerPosition(int listPos)393 private int getRingtoneManagerPosition(int listPos) { 394 return listPos - mStaticItemCount; 395 } 396 getListPosition(int ringtoneManagerPos)397 private int getListPosition(int ringtoneManagerPos) { 398 399 // If the manager position is -1 (for not found), return that 400 if (ringtoneManagerPos < 0) return ringtoneManagerPos; 401 402 return ringtoneManagerPos + mStaticItemCount; 403 } 404 405 private static class LocalizedCursor extends CursorWrapper { 406 407 final int mTitleIndex; 408 final Resources mResources; 409 String mNamePrefix; 410 final Pattern mSanitizePattern; 411 LocalizedCursor(Cursor cursor, Resources resources, String columnLabel)412 LocalizedCursor(Cursor cursor, Resources resources, String columnLabel) { 413 super(cursor); 414 mTitleIndex = mCursor.getColumnIndex(columnLabel); 415 mResources = resources; 416 mSanitizePattern = Pattern.compile("[^a-zA-Z0-9]"); 417 if (mTitleIndex == -1) { 418 Log.e(TAG, "No index for column " + columnLabel); 419 mNamePrefix = null; 420 } else { 421 try { 422 // Build the prefix for the name of the resource to look up 423 // format is: "ResourcePackageName::ResourceTypeName/" 424 // (the type name is expected to be "string" but let's not hardcode it). 425 // Here we use an existing resource "notification_sound_default" which is 426 // always expected to be found. 427 mNamePrefix = String.format("%s:%s/%s", 428 mResources.getResourcePackageName(R.string.notification_sound_default), 429 mResources.getResourceTypeName(R.string.notification_sound_default), 430 SOUND_NAME_RES_PREFIX); 431 } catch (NotFoundException e) { 432 mNamePrefix = null; 433 } 434 } 435 } 436 437 /** 438 * Process resource name to generate a valid resource name. 439 * @param input 440 * @return a non-null String 441 */ sanitize(String input)442 private String sanitize(String input) { 443 if (input == null) { 444 return ""; 445 } 446 return mSanitizePattern.matcher(input).replaceAll("_").toLowerCase(); 447 } 448 449 @Override getString(int columnIndex)450 public String getString(int columnIndex) { 451 final String defaultName = mCursor.getString(columnIndex); 452 if ((columnIndex != mTitleIndex) || (mNamePrefix == null)) { 453 return defaultName; 454 } 455 TypedValue value = new TypedValue(); 456 try { 457 // the name currently in the database is used to derive a name to match 458 // against resource names in this package 459 mResources.getValue(mNamePrefix + sanitize(defaultName), value, false); 460 } catch (NotFoundException e) { 461 // no localized string, use the default string 462 return defaultName; 463 } 464 if ((value != null) && (value.type == TypedValue.TYPE_STRING)) { 465 Log.d(TAG, String.format("Replacing name %s with %s", 466 defaultName, value.string.toString())); 467 return value.string.toString(); 468 } else { 469 Log.e(TAG, "Invalid value when looking up localized name, using " + defaultName); 470 return defaultName; 471 } 472 } 473 } 474 } 475