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