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