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