Home | History | Annotate | Download | only in media
      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                 setResultFromSelection();
    172             }
    173 
    174             // Play clip
    175             playRingtone(which, 0);
    176         }
    177 
    178     };
    179 
    180     @Override
    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
    265     public void onSaveInstanceState(Bundle outState) {
    266         super.onSaveInstanceState(outState);
    267         outState.putInt(SAVE_CLICKED_POS, getCheckedItem());
    268     }
    269 
    270     @Override
    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
    339     public void onDestroy() {
    340         if (mCursor != null) {
    341             mCursor.close();
    342             mCursor = null;
    343         }
    344         super.onDestroy();
    345     }
    346 
    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             setResultFromSelection();
    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      */
    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      */
    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 
    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 
    439     private int addSilentItem(ListView listView) {
    440         return addStaticItem(listView, com.android.internal.R.string.ringtone_silent);
    441     }
    442 
    443     private void addNewRingtoneItem(ListView listView) {
    444         listView.addFooterView(getLayoutInflater().inflate(R.layout.add_ringtone_item, listView,
    445                 false /* attachToRoot */));
    446     }
    447 
    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 
    458     private Ringtone getRingtone(int ringtoneManagerPosition) {
    459         if (ringtoneManagerPosition < 0) {
    460             return null;
    461         }
    462         return mRingtoneManager.getRingtone(ringtoneManagerPosition);
    463     }
    464 
    465     private int getCheckedItem() {
    466         return mAlertParams.mCheckedItem;
    467     }
    468 
    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      */
    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             setResultFromSelection();
    485         } else {
    486             setResult(RESULT_CANCELED);
    487         }
    488 
    489         finish();
    490     }
    491 
    492     /*
    493      * On item selected via keys
    494      */
    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             setResultFromSelection();
    502         }
    503     }
    504 
    505     public void onNothingSelected(AdapterView parent) {
    506     }
    507 
    508     private void playRingtone(int position, int delayMs) {
    509         mHandler.removeCallbacks(this);
    510         mSampleRingtonePos = position;
    511         mHandler.postDelayed(this, delayMs);
    512     }
    513 
    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
    551     protected void onStop() {
    552         super.onStop();
    553 
    554         if (!isChangingConfigurations()) {
    555             stopAnyPlayingRingtone();
    556         } else {
    557             saveAnyPlayingRingtone();
    558         }
    559     }
    560 
    561     @Override
    562     protected void onPause() {
    563         super.onPause();
    564         if (!isChangingConfigurations()) {
    565             stopAnyPlayingRingtone();
    566         }
    567     }
    568 
    569     private void setResultFromSelection() {
    570         // Obtain the currently selected ringtone
    571         Uri uri = null;
    572         if (getCheckedItem() == mDefaultRingtonePos) {
    573             // Set it to the default Uri that they originally gave us
    574             uri = mUriForDefaultItem;
    575         } else if (getCheckedItem() == mSilentPos) {
    576             // A null Uri is for the 'Silent' item
    577             uri = null;
    578         } else {
    579             uri = mRingtoneManager.getRingtoneUri(getRingtoneManagerPosition(getCheckedItem()));
    580         }
    581 
    582         // Return new URI if another ringtone was selected, as there's no ok/cancel button
    583         if (Objects.equals(uri, mExistingUri)) {
    584             setResult(RESULT_CANCELED);
    585         } else {
    586             Intent resultIntent = new Intent();
    587             resultIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, uri);
    588             setResult(RESULT_OK, resultIntent);
    589         }
    590     }
    591 
    592     private void saveAnyPlayingRingtone() {
    593         if (mDefaultRingtone != null && mDefaultRingtone.isPlaying()) {
    594             sPlayingRingtone = mDefaultRingtone;
    595         } else if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) {
    596             sPlayingRingtone = mCurrentRingtone;
    597         }
    598     }
    599 
    600     private void stopAnyPlayingRingtone() {
    601         if (sPlayingRingtone != null && sPlayingRingtone.isPlaying()) {
    602             sPlayingRingtone.stop();
    603         }
    604         sPlayingRingtone = null;
    605 
    606         if (mDefaultRingtone != null && mDefaultRingtone.isPlaying()) {
    607             mDefaultRingtone.stop();
    608         }
    609 
    610         if (mRingtoneManager != null) {
    611             mRingtoneManager.stopPreviousRingtone();
    612         }
    613     }
    614 
    615     private int getRingtoneManagerPosition(int listPos) {
    616         return listPos - mStaticItemCount;
    617     }
    618 
    619     private int getListPosition(int ringtoneManagerPos) {
    620 
    621         // If the manager position is -1 (for not found), return that
    622         if (ringtoneManagerPos < 0) return ringtoneManagerPos;
    623 
    624         return ringtoneManagerPos + mStaticItemCount;
    625     }
    626 
    627     private static class LocalizedCursor extends CursorWrapper {
    628 
    629         final int mTitleIndex;
    630         final Resources mResources;
    631         String mNamePrefix;
    632         final Pattern mSanitizePattern;
    633 
    634         LocalizedCursor(Cursor cursor, Resources resources, String columnLabel) {
    635             super(cursor);
    636             mTitleIndex = mCursor.getColumnIndex(columnLabel);
    637             mResources = resources;
    638             mSanitizePattern = Pattern.compile("[^a-zA-Z0-9]");
    639             if (mTitleIndex == -1) {
    640                 Log.e(TAG, "No index for column " + columnLabel);
    641                 mNamePrefix = null;
    642             } else {
    643                 try {
    644                     // Build the prefix for the name of the resource to look up
    645                     // format is: "ResourcePackageName::ResourceTypeName/"
    646                     // (the type name is expected to be "string" but let's not hardcode it).
    647                     // Here we use an existing resource "notification_sound_default" which is
    648                     // always expected to be found.
    649                     mNamePrefix = String.format("%s:%s/%s",
    650                             mResources.getResourcePackageName(R.string.notification_sound_default),
    651                             mResources.getResourceTypeName(R.string.notification_sound_default),
    652                             SOUND_NAME_RES_PREFIX);
    653                 } catch (NotFoundException e) {
    654                     mNamePrefix = null;
    655                 }
    656             }
    657         }
    658 
    659         /**
    660          * Process resource name to generate a valid resource name.
    661          * @param input
    662          * @return a non-null String
    663          */
    664         private String sanitize(String input) {
    665             if (input == null) {
    666                 return "";
    667             }
    668             return mSanitizePattern.matcher(input).replaceAll("_").toLowerCase();
    669         }
    670 
    671         @Override
    672         public String getString(int columnIndex) {
    673             final String defaultName = mCursor.getString(columnIndex);
    674             if ((columnIndex != mTitleIndex) || (mNamePrefix == null)) {
    675                 return defaultName;
    676             }
    677             TypedValue value = new TypedValue();
    678             try {
    679                 // the name currently in the database is used to derive a name to match
    680                 // against resource names in this package
    681                 mResources.getValue(mNamePrefix + sanitize(defaultName), value, false);
    682             } catch (NotFoundException e) {
    683                 // no localized string, use the default string
    684                 return defaultName;
    685             }
    686             if ((value != null) && (value.type == TypedValue.TYPE_STRING)) {
    687                 Log.d(TAG, String.format("Replacing name %s with %s",
    688                         defaultName, value.string.toString()));
    689                 return value.string.toString();
    690             } else {
    691                 Log.e(TAG, "Invalid value when looking up localized name, using " + defaultName);
    692                 return defaultName;
    693             }
    694         }
    695     }
    696 
    697     private class BadgedRingtoneAdapter extends CursorAdapter {
    698         private final boolean mIsManagedProfile;
    699 
    700         public BadgedRingtoneAdapter(Context context, Cursor cursor, boolean isManagedProfile) {
    701             super(context, cursor);
    702             mIsManagedProfile = isManagedProfile;
    703         }
    704 
    705         @Override
    706         public long getItemId(int position) {
    707             if (position < 0) {
    708                 return position;
    709             }
    710             return super.getItemId(position);
    711         }
    712 
    713         @Override
    714         public View newView(Context context, Cursor cursor, ViewGroup parent) {
    715             LayoutInflater inflater = LayoutInflater.from(context);
    716             return inflater.inflate(R.layout.radio_with_work_badge, parent, false);
    717         }
    718 
    719         @Override
    720         public void bindView(View view, Context context, Cursor cursor) {
    721             // Set text as the title of the ringtone
    722             ((TextView) view.findViewById(R.id.checked_text_view))
    723                     .setText(cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX));
    724 
    725             boolean isWorkRingtone = false;
    726             if (mIsManagedProfile) {
    727                 /*
    728                  * Display the work icon if the ringtone belongs to a work profile. We can tell that
    729                  * a ringtone belongs to a work profile if the picker user is a managed profile, the
    730                  * ringtone Uri is in external storage, and either the uri has no user id or has the
    731                  * id of the picker user
    732                  */
    733                 Uri currentUri = mRingtoneManager.getRingtoneUri(cursor.getPosition());
    734                 int uriUserId = ContentProvider.getUserIdFromUri(currentUri, mPickerUserId);
    735                 Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(currentUri);
    736 
    737                 if (uriUserId == mPickerUserId && uriWithoutUserId.toString()
    738                         .startsWith(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) {
    739                     isWorkRingtone = true;
    740                 }
    741             }
    742 
    743             ImageView workIcon = (ImageView) view.findViewById(R.id.work_icon);
    744             if(isWorkRingtone) {
    745                 workIcon.setImageDrawable(getPackageManager().getUserBadgeForDensityNoBackground(
    746                         UserHandle.of(mPickerUserId), -1 /* density */));
    747                 workIcon.setVisibility(View.VISIBLE);
    748             } else {
    749                 workIcon.setVisibility(View.GONE);
    750             }
    751         }
    752     }
    753 }
    754