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.ContentProvider; 20 import android.content.Context; 21 import android.content.DialogInterface; 22 import android.content.Intent; 23 import android.content.res.Resources; 24 import android.content.res.Resources.NotFoundException; 25 import android.database.Cursor; 26 import android.database.CursorWrapper; 27 import android.media.AudioAttributes; 28 import android.media.Ringtone; 29 import android.media.RingtoneManager; 30 import android.net.Uri; 31 import android.os.AsyncTask; 32 import android.os.Bundle; 33 import android.os.Environment; 34 import android.os.Handler; 35 import android.os.UserHandle; 36 import android.os.UserManager; 37 import android.provider.MediaStore; 38 import android.provider.Settings; 39 import android.util.Log; 40 import android.util.TypedValue; 41 import android.view.LayoutInflater; 42 import android.view.View; 43 import android.view.ViewGroup; 44 import android.widget.AdapterView; 45 import android.widget.CursorAdapter; 46 import android.widget.ImageView; 47 import android.widget.ListView; 48 import android.widget.TextView; 49 import android.widget.Toast; 50 51 import com.android.internal.app.AlertActivity; 52 import com.android.internal.app.AlertController; 53 54 import java.io.IOException; 55 import java.util.Objects; 56 import java.util.regex.Pattern; 57 58 /** 59 * The {@link RingtonePickerActivity} allows the user to choose one from all of the 60 * available ringtones. The chosen ringtone's URI will be persisted as a string. 61 * 62 * @see RingtoneManager#ACTION_RINGTONE_PICKER 63 */ 64 public final class RingtonePickerActivity extends AlertActivity implements 65 AdapterView.OnItemSelectedListener, Runnable, DialogInterface.OnClickListener, 66 AlertController.AlertParams.OnPrepareListViewListener { 67 68 private static final int POS_UNKNOWN = -1; 69 70 private static final String TAG = "RingtonePickerActivity"; 71 72 private static final int DELAY_MS_SELECTION_PLAYED = 300; 73 74 private static final String COLUMN_LABEL = MediaStore.Audio.Media.TITLE; 75 76 private static final String SAVE_CLICKED_POS = "clicked_pos"; 77 78 private static final String SOUND_NAME_RES_PREFIX = "sound_name_"; 79 80 private static final int ADD_FILE_REQUEST_CODE = 300; 81 82 private RingtoneManager mRingtoneManager; 83 private int mType; 84 85 private Cursor mCursor; 86 private Handler mHandler; 87 private BadgedRingtoneAdapter mAdapter; 88 89 /** The position in the list of the 'Silent' item. */ 90 private int mSilentPos = POS_UNKNOWN; 91 92 /** The position in the list of the 'Default' item. */ 93 private int mDefaultRingtonePos = POS_UNKNOWN; 94 95 /** The position in the list of the ringtone to sample. */ 96 private int mSampleRingtonePos = POS_UNKNOWN; 97 98 /** Whether this list has the 'Silent' item. */ 99 private boolean mHasSilentItem; 100 101 /** The Uri to place a checkmark next to. */ 102 private Uri mExistingUri; 103 104 /** The number of static items in the list. */ 105 private int mStaticItemCount; 106 107 /** Whether this list has the 'Default' item. */ 108 private boolean mHasDefaultItem; 109 110 /** The Uri to play when the 'Default' item is clicked. */ 111 private Uri mUriForDefaultItem; 112 113 /** Id of the user to which the ringtone picker should list the ringtones */ 114 private int mPickerUserId; 115 116 /** Context of the user specified by mPickerUserId */ 117 private Context mTargetContext; 118 119 /** 120 * A Ringtone for the default ringtone. In most cases, the RingtoneManager 121 * will stop the previous ringtone. However, the RingtoneManager doesn't 122 * manage the default ringtone for us, so we should stop this one manually. 123 */ 124 private Ringtone mDefaultRingtone; 125 126 /** 127 * The ringtone that's currently playing, unless the currently playing one is the default 128 * ringtone. 129 */ 130 private Ringtone mCurrentRingtone; 131 132 /** 133 * Stable ID for the ringtone that is currently checked (may be -1 if no ringtone is checked). 134 */ 135 private long mCheckedItemId = -1; 136 137 private int mAttributesFlags; 138 139 private boolean mShowOkCancelButtons; 140 141 /** 142 * Keep the currently playing ringtone around when changing orientation, so that it 143 * can be stopped later, after the activity is recreated. 144 */ 145 private static Ringtone sPlayingRingtone; 146 147 private DialogInterface.OnClickListener mRingtoneClickListener = 148 new DialogInterface.OnClickListener() { 149 150 /* 151 * On item clicked 152 */ 153 public void onClick(DialogInterface dialog, int which) { 154 if (which == mCursor.getCount() + mStaticItemCount) { 155 // The "Add new ringtone" item was clicked. Start a file picker intent to select 156 // only audio files (MIME type "audio/*") 157 final Intent chooseFile = new Intent(Intent.ACTION_GET_CONTENT); 158 chooseFile.setType("audio/*"); 159 chooseFile.putExtra(Intent.EXTRA_MIME_TYPES, 160 new String[] { "audio/*", "application/ogg" }); 161 startActivityForResult(chooseFile, ADD_FILE_REQUEST_CODE); 162 return; 163 } 164 165 // Save the position of most recently clicked item 166 setCheckedItem(which); 167 168 // In the buttonless (watch-only) version, preemptively set our result since we won't 169 // have another chance to do so before the activity closes. 170 if (!mShowOkCancelButtons) { 171 setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri()); 172 } 173 174 // Play clip 175 playRingtone(which, 0); 176 } 177 178 }; 179 180 @Override onCreate(Bundle savedInstanceState)181 protected void onCreate(Bundle savedInstanceState) { 182 super.onCreate(savedInstanceState); 183 184 mHandler = new Handler(); 185 186 Intent intent = getIntent(); 187 mPickerUserId = UserHandle.myUserId(); 188 mTargetContext = this; 189 190 // Get the types of ringtones to show 191 mType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, -1); 192 initRingtoneManager(); 193 194 /* 195 * Get whether to show the 'Default' item, and the URI to play when the 196 * default is clicked 197 */ 198 mHasDefaultItem = intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); 199 mUriForDefaultItem = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI); 200 if (mUriForDefaultItem == null) { 201 if (mType == RingtoneManager.TYPE_NOTIFICATION) { 202 mUriForDefaultItem = Settings.System.DEFAULT_NOTIFICATION_URI; 203 } else if (mType == RingtoneManager.TYPE_ALARM) { 204 mUriForDefaultItem = Settings.System.DEFAULT_ALARM_ALERT_URI; 205 } else if (mType == RingtoneManager.TYPE_RINGTONE) { 206 mUriForDefaultItem = Settings.System.DEFAULT_RINGTONE_URI; 207 } else { 208 // or leave it null for silence. 209 mUriForDefaultItem = Settings.System.DEFAULT_RINGTONE_URI; 210 } 211 } 212 213 // Get whether to show the 'Silent' item 214 mHasSilentItem = intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); 215 // AudioAttributes flags 216 mAttributesFlags |= intent.getIntExtra( 217 RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS, 218 0 /*defaultValue == no flags*/); 219 220 mShowOkCancelButtons = getResources().getBoolean(R.bool.config_showOkCancelButtons); 221 222 // The volume keys will control the stream that we are choosing a ringtone for 223 setVolumeControlStream(mRingtoneManager.inferStreamType()); 224 225 // Get the URI whose list item should have a checkmark 226 mExistingUri = intent 227 .getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI); 228 229 // Create the list of ringtones and hold on to it so we can update later. 230 mAdapter = new BadgedRingtoneAdapter(this, mCursor, 231 /* isManagedProfile = */ UserManager.get(this).isManagedProfile(mPickerUserId)); 232 if (savedInstanceState != null) { 233 setCheckedItem(savedInstanceState.getInt(SAVE_CLICKED_POS, POS_UNKNOWN)); 234 } 235 236 final AlertController.AlertParams p = mAlertParams; 237 p.mAdapter = mAdapter; 238 p.mOnClickListener = mRingtoneClickListener; 239 p.mLabelColumn = COLUMN_LABEL; 240 p.mIsSingleChoice = true; 241 p.mOnItemSelectedListener = this; 242 if (mShowOkCancelButtons) { 243 p.mPositiveButtonText = getString(com.android.internal.R.string.ok); 244 p.mPositiveButtonListener = this; 245 p.mNegativeButtonText = getString(com.android.internal.R.string.cancel); 246 p.mPositiveButtonListener = this; 247 } 248 p.mOnPrepareListViewListener = this; 249 250 p.mTitle = intent.getCharSequenceExtra(RingtoneManager.EXTRA_RINGTONE_TITLE); 251 if (p.mTitle == null) { 252 if (mType == RingtoneManager.TYPE_ALARM) { 253 p.mTitle = getString(com.android.internal.R.string.ringtone_picker_title_alarm); 254 } else if (mType == RingtoneManager.TYPE_NOTIFICATION) { 255 p.mTitle = 256 getString(com.android.internal.R.string.ringtone_picker_title_notification); 257 } else { 258 p.mTitle = getString(com.android.internal.R.string.ringtone_picker_title); 259 } 260 } 261 262 setupAlert(); 263 } 264 @Override onSaveInstanceState(Bundle outState)265 public void onSaveInstanceState(Bundle outState) { 266 super.onSaveInstanceState(outState); 267 outState.putInt(SAVE_CLICKED_POS, getCheckedItem()); 268 } 269 270 @Override onActivityResult(int requestCode, int resultCode, Intent data)271 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 272 super.onActivityResult(requestCode, resultCode, data); 273 274 if (requestCode == ADD_FILE_REQUEST_CODE && resultCode == RESULT_OK) { 275 // Add the custom ringtone in a separate thread 276 final AsyncTask<Uri, Void, Uri> installTask = new AsyncTask<Uri, Void, Uri>() { 277 @Override 278 protected Uri doInBackground(Uri... params) { 279 try { 280 return mRingtoneManager.addCustomExternalRingtone(params[0], mType); 281 } catch (IOException | IllegalArgumentException e) { 282 Log.e(TAG, "Unable to add new ringtone", e); 283 } 284 return null; 285 } 286 287 @Override 288 protected void onPostExecute(Uri ringtoneUri) { 289 if (ringtoneUri != null) { 290 requeryForAdapter(); 291 } else { 292 // Ringtone was not added, display error Toast 293 Toast.makeText(RingtonePickerActivity.this, R.string.unable_to_add_ringtone, 294 Toast.LENGTH_SHORT).show(); 295 } 296 } 297 }; 298 installTask.execute(data.getData()); 299 } 300 } 301 302 // Disabled because context menus aren't Material Design :( 303 /* 304 @Override 305 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 306 int position = ((AdapterContextMenuInfo) menuInfo).position; 307 308 Ringtone ringtone = getRingtone(getRingtoneManagerPosition(position)); 309 if (ringtone != null && mRingtoneManager.isCustomRingtone(ringtone.getUri())) { 310 // It's a custom ringtone so we display the context menu 311 menu.setHeaderTitle(ringtone.getTitle(this)); 312 menu.add(Menu.NONE, Menu.FIRST, Menu.NONE, R.string.delete_ringtone_text); 313 } 314 } 315 316 @Override 317 public boolean onContextItemSelected(MenuItem item) { 318 switch (item.getItemId()) { 319 case Menu.FIRST: { 320 int deletedRingtonePos = ((AdapterContextMenuInfo) item.getMenuInfo()).position; 321 Uri deletedRingtoneUri = getRingtone( 322 getRingtoneManagerPosition(deletedRingtonePos)).getUri(); 323 if(mRingtoneManager.deleteExternalRingtone(deletedRingtoneUri)) { 324 requeryForAdapter(); 325 } else { 326 Toast.makeText(this, R.string.unable_to_delete_ringtone, Toast.LENGTH_SHORT) 327 .show(); 328 } 329 return true; 330 } 331 default: { 332 return false; 333 } 334 } 335 } 336 */ 337 338 @Override onDestroy()339 public void onDestroy() { 340 if (mCursor != null) { 341 mCursor.close(); 342 mCursor = null; 343 } 344 super.onDestroy(); 345 } 346 onPrepareListView(ListView listView)347 public void onPrepareListView(ListView listView) { 348 // Reset the static item count, as this method can be called multiple times 349 mStaticItemCount = 0; 350 351 if (mHasDefaultItem) { 352 mDefaultRingtonePos = addDefaultRingtoneItem(listView); 353 354 if (getCheckedItem() == POS_UNKNOWN && RingtoneManager.isDefault(mExistingUri)) { 355 setCheckedItem(mDefaultRingtonePos); 356 } 357 } 358 359 if (mHasSilentItem) { 360 mSilentPos = addSilentItem(listView); 361 362 // The 'Silent' item should use a null Uri 363 if (getCheckedItem() == POS_UNKNOWN && mExistingUri == null) { 364 setCheckedItem(mSilentPos); 365 } 366 } 367 368 if (getCheckedItem() == POS_UNKNOWN) { 369 setCheckedItem(getListPosition(mRingtoneManager.getRingtonePosition(mExistingUri))); 370 } 371 372 // In the buttonless (watch-only) version, preemptively set our result since we won't 373 // have another chance to do so before the activity closes. 374 if (!mShowOkCancelButtons) { 375 setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri()); 376 } 377 // If external storage is available, add a button to install sounds from storage. 378 if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { 379 addNewRingtoneItem(listView); 380 } 381 382 // Enable context menu in ringtone items 383 registerForContextMenu(listView); 384 } 385 386 /** 387 * Re-query RingtoneManager for the most recent set of installed ringtones. May move the 388 * selected item position to match the new position of the chosen sound. 389 * 390 * This should only need to happen after adding or removing a ringtone. 391 */ requeryForAdapter()392 private void requeryForAdapter() { 393 // Refresh and set a new cursor, closing the old one. 394 initRingtoneManager(); 395 mAdapter.changeCursor(mCursor); 396 397 // Update checked item location. 398 int checkedPosition = POS_UNKNOWN; 399 for (int i = 0; i < mAdapter.getCount(); i++) { 400 if (mAdapter.getItemId(i) == mCheckedItemId) { 401 checkedPosition = getListPosition(i); 402 break; 403 } 404 } 405 if (mHasSilentItem && checkedPosition == POS_UNKNOWN) { 406 checkedPosition = mSilentPos; 407 } 408 setCheckedItem(checkedPosition); 409 setupAlert(); 410 } 411 412 /** 413 * Adds a static item to the top of the list. A static item is one that is not from the 414 * RingtoneManager. 415 * 416 * @param listView The ListView to add to. 417 * @param textResId The resource ID of the text for the item. 418 * @return The position of the inserted item. 419 */ addStaticItem(ListView listView, int textResId)420 private int addStaticItem(ListView listView, int textResId) { 421 TextView textView = (TextView) getLayoutInflater().inflate( 422 com.android.internal.R.layout.select_dialog_singlechoice_material, listView, false); 423 textView.setText(textResId); 424 listView.addHeaderView(textView); 425 mStaticItemCount++; 426 return listView.getHeaderViewsCount() - 1; 427 } 428 addDefaultRingtoneItem(ListView listView)429 private int addDefaultRingtoneItem(ListView listView) { 430 if (mType == RingtoneManager.TYPE_NOTIFICATION) { 431 return addStaticItem(listView, R.string.notification_sound_default); 432 } else if (mType == RingtoneManager.TYPE_ALARM) { 433 return addStaticItem(listView, R.string.alarm_sound_default); 434 } 435 436 return addStaticItem(listView, R.string.ringtone_default); 437 } 438 addSilentItem(ListView listView)439 private int addSilentItem(ListView listView) { 440 return addStaticItem(listView, com.android.internal.R.string.ringtone_silent); 441 } 442 addNewRingtoneItem(ListView listView)443 private void addNewRingtoneItem(ListView listView) { 444 listView.addFooterView(getLayoutInflater().inflate(R.layout.add_ringtone_item, listView, 445 false /* attachToRoot */)); 446 } 447 initRingtoneManager()448 private void initRingtoneManager() { 449 // Reinstantiate the RingtoneManager. Cursor.requery() was deprecated and calling it 450 // causes unexpected behavior. 451 mRingtoneManager = new RingtoneManager(mTargetContext, /* includeParentRingtones */ true); 452 if (mType != -1) { 453 mRingtoneManager.setType(mType); 454 } 455 mCursor = new LocalizedCursor(mRingtoneManager.getCursor(), getResources(), COLUMN_LABEL); 456 } 457 getRingtone(int ringtoneManagerPosition)458 private Ringtone getRingtone(int ringtoneManagerPosition) { 459 if (ringtoneManagerPosition < 0) { 460 return null; 461 } 462 return mRingtoneManager.getRingtone(ringtoneManagerPosition); 463 } 464 getCheckedItem()465 private int getCheckedItem() { 466 return mAlertParams.mCheckedItem; 467 } 468 setCheckedItem(int pos)469 private void setCheckedItem(int pos) { 470 mAlertParams.mCheckedItem = pos; 471 mCheckedItemId = mAdapter.getItemId(getRingtoneManagerPosition(pos)); 472 } 473 474 /* 475 * On click of Ok/Cancel buttons 476 */ onClick(DialogInterface dialog, int which)477 public void onClick(DialogInterface dialog, int which) { 478 boolean positiveResult = which == DialogInterface.BUTTON_POSITIVE; 479 480 // Stop playing the previous ringtone 481 mRingtoneManager.stopPreviousRingtone(); 482 483 if (positiveResult) { 484 setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri()); 485 } else { 486 setResult(RESULT_CANCELED); 487 } 488 489 finish(); 490 } 491 492 /* 493 * On item selected via keys 494 */ onItemSelected(AdapterView parent, View view, int position, long id)495 public void onItemSelected(AdapterView parent, View view, int position, long id) { 496 playRingtone(position, DELAY_MS_SELECTION_PLAYED); 497 498 // In the buttonless (watch-only) version, preemptively set our result since we won't 499 // have another chance to do so before the activity closes. 500 if (!mShowOkCancelButtons) { 501 setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri()); 502 } 503 } 504 onNothingSelected(AdapterView parent)505 public void onNothingSelected(AdapterView parent) { 506 } 507 playRingtone(int position, int delayMs)508 private void playRingtone(int position, int delayMs) { 509 mHandler.removeCallbacks(this); 510 mSampleRingtonePos = position; 511 mHandler.postDelayed(this, delayMs); 512 } 513 run()514 public void run() { 515 stopAnyPlayingRingtone(); 516 if (mSampleRingtonePos == mSilentPos) { 517 return; 518 } 519 520 Ringtone ringtone; 521 if (mSampleRingtonePos == mDefaultRingtonePos) { 522 if (mDefaultRingtone == null) { 523 mDefaultRingtone = RingtoneManager.getRingtone(this, mUriForDefaultItem); 524 } 525 /* 526 * Stream type of mDefaultRingtone is not set explicitly here. 527 * It should be set in accordance with mRingtoneManager of this Activity. 528 */ 529 if (mDefaultRingtone != null) { 530 mDefaultRingtone.setStreamType(mRingtoneManager.inferStreamType()); 531 } 532 ringtone = mDefaultRingtone; 533 mCurrentRingtone = null; 534 } else { 535 ringtone = mRingtoneManager.getRingtone(getRingtoneManagerPosition(mSampleRingtonePos)); 536 mCurrentRingtone = ringtone; 537 } 538 539 if (ringtone != null) { 540 if (mAttributesFlags != 0) { 541 ringtone.setAudioAttributes( 542 new AudioAttributes.Builder(ringtone.getAudioAttributes()) 543 .setFlags(mAttributesFlags) 544 .build()); 545 } 546 ringtone.play(); 547 } 548 } 549 550 @Override onStop()551 protected void onStop() { 552 super.onStop(); 553 554 if (!isChangingConfigurations()) { 555 stopAnyPlayingRingtone(); 556 } else { 557 saveAnyPlayingRingtone(); 558 } 559 } 560 561 @Override onPause()562 protected void onPause() { 563 super.onPause(); 564 if (!isChangingConfigurations()) { 565 stopAnyPlayingRingtone(); 566 } 567 } 568 setSuccessResultWithRingtone(Uri ringtoneUri)569 private void setSuccessResultWithRingtone(Uri ringtoneUri) { 570 setResult(RESULT_OK, 571 new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, ringtoneUri)); 572 } 573 getCurrentlySelectedRingtoneUri()574 private Uri getCurrentlySelectedRingtoneUri() { 575 if (getCheckedItem() == mDefaultRingtonePos) { 576 // Use the default Uri that they originally gave us. 577 return mUriForDefaultItem; 578 } else if (getCheckedItem() == mSilentPos) { 579 // Use a null Uri for the 'Silent' item. 580 return null; 581 } else { 582 return mRingtoneManager.getRingtoneUri(getRingtoneManagerPosition(getCheckedItem())); 583 } 584 } 585 saveAnyPlayingRingtone()586 private void saveAnyPlayingRingtone() { 587 if (mDefaultRingtone != null && mDefaultRingtone.isPlaying()) { 588 sPlayingRingtone = mDefaultRingtone; 589 } else if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) { 590 sPlayingRingtone = mCurrentRingtone; 591 } 592 } 593 stopAnyPlayingRingtone()594 private void stopAnyPlayingRingtone() { 595 if (sPlayingRingtone != null && sPlayingRingtone.isPlaying()) { 596 sPlayingRingtone.stop(); 597 } 598 sPlayingRingtone = null; 599 600 if (mDefaultRingtone != null && mDefaultRingtone.isPlaying()) { 601 mDefaultRingtone.stop(); 602 } 603 604 if (mRingtoneManager != null) { 605 mRingtoneManager.stopPreviousRingtone(); 606 } 607 } 608 getRingtoneManagerPosition(int listPos)609 private int getRingtoneManagerPosition(int listPos) { 610 return listPos - mStaticItemCount; 611 } 612 getListPosition(int ringtoneManagerPos)613 private int getListPosition(int ringtoneManagerPos) { 614 615 // If the manager position is -1 (for not found), return that 616 if (ringtoneManagerPos < 0) return ringtoneManagerPos; 617 618 return ringtoneManagerPos + mStaticItemCount; 619 } 620 621 private static class LocalizedCursor extends CursorWrapper { 622 623 final int mTitleIndex; 624 final Resources mResources; 625 String mNamePrefix; 626 final Pattern mSanitizePattern; 627 LocalizedCursor(Cursor cursor, Resources resources, String columnLabel)628 LocalizedCursor(Cursor cursor, Resources resources, String columnLabel) { 629 super(cursor); 630 mTitleIndex = mCursor.getColumnIndex(columnLabel); 631 mResources = resources; 632 mSanitizePattern = Pattern.compile("[^a-zA-Z0-9]"); 633 if (mTitleIndex == -1) { 634 Log.e(TAG, "No index for column " + columnLabel); 635 mNamePrefix = null; 636 } else { 637 try { 638 // Build the prefix for the name of the resource to look up 639 // format is: "ResourcePackageName::ResourceTypeName/" 640 // (the type name is expected to be "string" but let's not hardcode it). 641 // Here we use an existing resource "notification_sound_default" which is 642 // always expected to be found. 643 mNamePrefix = String.format("%s:%s/%s", 644 mResources.getResourcePackageName(R.string.notification_sound_default), 645 mResources.getResourceTypeName(R.string.notification_sound_default), 646 SOUND_NAME_RES_PREFIX); 647 } catch (NotFoundException e) { 648 mNamePrefix = null; 649 } 650 } 651 } 652 653 /** 654 * Process resource name to generate a valid resource name. 655 * @param input 656 * @return a non-null String 657 */ sanitize(String input)658 private String sanitize(String input) { 659 if (input == null) { 660 return ""; 661 } 662 return mSanitizePattern.matcher(input).replaceAll("_").toLowerCase(); 663 } 664 665 @Override getString(int columnIndex)666 public String getString(int columnIndex) { 667 final String defaultName = mCursor.getString(columnIndex); 668 if ((columnIndex != mTitleIndex) || (mNamePrefix == null)) { 669 return defaultName; 670 } 671 TypedValue value = new TypedValue(); 672 try { 673 // the name currently in the database is used to derive a name to match 674 // against resource names in this package 675 mResources.getValue(mNamePrefix + sanitize(defaultName), value, false); 676 } catch (NotFoundException e) { 677 // no localized string, use the default string 678 return defaultName; 679 } 680 if ((value != null) && (value.type == TypedValue.TYPE_STRING)) { 681 Log.d(TAG, String.format("Replacing name %s with %s", 682 defaultName, value.string.toString())); 683 return value.string.toString(); 684 } else { 685 Log.e(TAG, "Invalid value when looking up localized name, using " + defaultName); 686 return defaultName; 687 } 688 } 689 } 690 691 private class BadgedRingtoneAdapter extends CursorAdapter { 692 private final boolean mIsManagedProfile; 693 BadgedRingtoneAdapter(Context context, Cursor cursor, boolean isManagedProfile)694 public BadgedRingtoneAdapter(Context context, Cursor cursor, boolean isManagedProfile) { 695 super(context, cursor); 696 mIsManagedProfile = isManagedProfile; 697 } 698 699 @Override getItemId(int position)700 public long getItemId(int position) { 701 if (position < 0) { 702 return position; 703 } 704 return super.getItemId(position); 705 } 706 707 @Override newView(Context context, Cursor cursor, ViewGroup parent)708 public View newView(Context context, Cursor cursor, ViewGroup parent) { 709 LayoutInflater inflater = LayoutInflater.from(context); 710 return inflater.inflate(R.layout.radio_with_work_badge, parent, false); 711 } 712 713 @Override bindView(View view, Context context, Cursor cursor)714 public void bindView(View view, Context context, Cursor cursor) { 715 // Set text as the title of the ringtone 716 ((TextView) view.findViewById(R.id.checked_text_view)) 717 .setText(cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX)); 718 719 boolean isWorkRingtone = false; 720 if (mIsManagedProfile) { 721 /* 722 * Display the work icon if the ringtone belongs to a work profile. We can tell that 723 * a ringtone belongs to a work profile if the picker user is a managed profile, the 724 * ringtone Uri is in external storage, and either the uri has no user id or has the 725 * id of the picker user 726 */ 727 Uri currentUri = mRingtoneManager.getRingtoneUri(cursor.getPosition()); 728 int uriUserId = ContentProvider.getUserIdFromUri(currentUri, mPickerUserId); 729 Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(currentUri); 730 731 if (uriUserId == mPickerUserId && uriWithoutUserId.toString() 732 .startsWith(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) { 733 isWorkRingtone = true; 734 } 735 } 736 737 ImageView workIcon = (ImageView) view.findViewById(R.id.work_icon); 738 if(isWorkRingtone) { 739 workIcon.setImageDrawable(getPackageManager().getUserBadgeForDensityNoBackground( 740 UserHandle.of(mPickerUserId), -1 /* density */)); 741 workIcon.setVisibility(View.VISIBLE); 742 } else { 743 workIcon.setVisibility(View.GONE); 744 } 745 } 746 } 747 } 748