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