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