• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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