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