• 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.providers.media;
18 
19 import android.content.ContentProvider;
20 import android.content.Context;
21 import android.content.DialogInterface;
22 import android.content.Intent;
23 import android.content.res.Resources;
24 import android.content.res.Resources.NotFoundException;
25 import android.database.Cursor;
26 import android.database.CursorWrapper;
27 import android.media.AudioAttributes;
28 import android.media.Ringtone;
29 import android.media.RingtoneManager;
30 import android.net.Uri;
31 import android.os.AsyncTask;
32 import android.os.Bundle;
33 import android.os.Environment;
34 import android.os.Handler;
35 import android.os.UserHandle;
36 import android.os.UserManager;
37 import android.provider.MediaStore;
38 import android.provider.Settings;
39 import android.util.Log;
40 import android.util.TypedValue;
41 import android.view.LayoutInflater;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.widget.AdapterView;
45 import android.widget.CursorAdapter;
46 import android.widget.ImageView;
47 import android.widget.ListView;
48 import android.widget.TextView;
49 import android.widget.Toast;
50 
51 import com.android.internal.app.AlertActivity;
52 import com.android.internal.app.AlertController;
53 
54 import java.io.IOException;
55 import java.util.Objects;
56 import java.util.regex.Pattern;
57 
58 /**
59  * The {@link RingtonePickerActivity} allows the user to choose one from all of the
60  * available ringtones. The chosen ringtone's URI will be persisted as a string.
61  *
62  * @see RingtoneManager#ACTION_RINGTONE_PICKER
63  */
64 public final class RingtonePickerActivity extends AlertActivity implements
65         AdapterView.OnItemSelectedListener, Runnable, DialogInterface.OnClickListener,
66         AlertController.AlertParams.OnPrepareListViewListener {
67 
68     private static final int POS_UNKNOWN = -1;
69 
70     private static final String TAG = "RingtonePickerActivity";
71 
72     private static final int DELAY_MS_SELECTION_PLAYED = 300;
73 
74     private static final String COLUMN_LABEL = MediaStore.Audio.Media.TITLE;
75 
76     private static final String SAVE_CLICKED_POS = "clicked_pos";
77 
78     private static final String SOUND_NAME_RES_PREFIX = "sound_name_";
79 
80     private static final int ADD_FILE_REQUEST_CODE = 300;
81 
82     private RingtoneManager mRingtoneManager;
83     private int mType;
84 
85     private Cursor mCursor;
86     private Handler mHandler;
87     private BadgedRingtoneAdapter mAdapter;
88 
89     /** The position in the list of the 'Silent' item. */
90     private int mSilentPos = POS_UNKNOWN;
91 
92     /** The position in the list of the 'Default' item. */
93     private int mDefaultRingtonePos = POS_UNKNOWN;
94 
95     /** The position in the list of the ringtone to sample. */
96     private int mSampleRingtonePos = POS_UNKNOWN;
97 
98     /** Whether this list has the 'Silent' item. */
99     private boolean mHasSilentItem;
100 
101     /** The Uri to place a checkmark next to. */
102     private Uri mExistingUri;
103 
104     /** The number of static items in the list. */
105     private int mStaticItemCount;
106 
107     /** Whether this list has the 'Default' item. */
108     private boolean mHasDefaultItem;
109 
110     /** The Uri to play when the 'Default' item is clicked. */
111     private Uri mUriForDefaultItem;
112 
113     /** Id of the user to which the ringtone picker should list the ringtones */
114     private int mPickerUserId;
115 
116     /** Context of the user specified by mPickerUserId */
117     private Context mTargetContext;
118 
119     /**
120      * A Ringtone for the default ringtone. In most cases, the RingtoneManager
121      * will stop the previous ringtone. However, the RingtoneManager doesn't
122      * manage the default ringtone for us, so we should stop this one manually.
123      */
124     private Ringtone mDefaultRingtone;
125 
126     /**
127      * The ringtone that's currently playing, unless the currently playing one is the default
128      * ringtone.
129      */
130     private Ringtone mCurrentRingtone;
131 
132     /**
133      * Stable ID for the ringtone that is currently checked (may be -1 if no ringtone is checked).
134      */
135     private long mCheckedItemId = -1;
136 
137     private int mAttributesFlags;
138 
139     private boolean mShowOkCancelButtons;
140 
141     /**
142      * Keep the currently playing ringtone around when changing orientation, so that it
143      * can be stopped later, after the activity is recreated.
144      */
145     private static Ringtone sPlayingRingtone;
146 
147     private DialogInterface.OnClickListener mRingtoneClickListener =
148             new DialogInterface.OnClickListener() {
149 
150         /*
151          * On item clicked
152          */
153         public void onClick(DialogInterface dialog, int which) {
154             if (which == mCursor.getCount() + mStaticItemCount) {
155                 // The "Add new ringtone" item was clicked. Start a file picker intent to select
156                 // only audio files (MIME type "audio/*")
157                 final Intent chooseFile = new Intent(Intent.ACTION_GET_CONTENT);
158                 chooseFile.setType("audio/*");
159                 chooseFile.putExtra(Intent.EXTRA_MIME_TYPES,
160                         new String[] { "audio/*", "application/ogg" });
161                 startActivityForResult(chooseFile, ADD_FILE_REQUEST_CODE);
162                 return;
163             }
164 
165             // Save the position of most recently clicked item
166             setCheckedItem(which);
167 
168             // In the buttonless (watch-only) version, preemptively set our result since we won't
169             // have another chance to do so before the activity closes.
170             if (!mShowOkCancelButtons) {
171                 setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
172             }
173 
174             // Play clip
175             playRingtone(which, 0);
176         }
177 
178     };
179 
180     @Override
onCreate(Bundle savedInstanceState)181     protected void onCreate(Bundle savedInstanceState) {
182         super.onCreate(savedInstanceState);
183 
184         mHandler = new Handler();
185 
186         Intent intent = getIntent();
187         mPickerUserId = UserHandle.myUserId();
188         mTargetContext = this;
189 
190         // Get the types of ringtones to show
191         mType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, -1);
192         initRingtoneManager();
193 
194         /*
195          * Get whether to show the 'Default' item, and the URI to play when the
196          * default is clicked
197          */
198         mHasDefaultItem = intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
199         mUriForDefaultItem = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI);
200         if (mUriForDefaultItem == null) {
201             if (mType == RingtoneManager.TYPE_NOTIFICATION) {
202                 mUriForDefaultItem = Settings.System.DEFAULT_NOTIFICATION_URI;
203             } else if (mType == RingtoneManager.TYPE_ALARM) {
204                 mUriForDefaultItem = Settings.System.DEFAULT_ALARM_ALERT_URI;
205             } else if (mType == RingtoneManager.TYPE_RINGTONE) {
206                 mUriForDefaultItem = Settings.System.DEFAULT_RINGTONE_URI;
207             } else {
208                 // or leave it null for silence.
209                 mUriForDefaultItem = Settings.System.DEFAULT_RINGTONE_URI;
210             }
211         }
212 
213         // Get whether to show the 'Silent' item
214         mHasSilentItem = intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
215         // AudioAttributes flags
216         mAttributesFlags |= intent.getIntExtra(
217                 RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS,
218                 0 /*defaultValue == no flags*/);
219 
220         mShowOkCancelButtons = getResources().getBoolean(R.bool.config_showOkCancelButtons);
221 
222         // The volume keys will control the stream that we are choosing a ringtone for
223         setVolumeControlStream(mRingtoneManager.inferStreamType());
224 
225         // Get the URI whose list item should have a checkmark
226         mExistingUri = intent
227                 .getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
228 
229         // Create the list of ringtones and hold on to it so we can update later.
230         mAdapter = new BadgedRingtoneAdapter(this, mCursor,
231                 /* isManagedProfile = */ UserManager.get(this).isManagedProfile(mPickerUserId));
232         if (savedInstanceState != null) {
233             setCheckedItem(savedInstanceState.getInt(SAVE_CLICKED_POS, POS_UNKNOWN));
234         }
235 
236         final AlertController.AlertParams p = mAlertParams;
237         p.mAdapter = mAdapter;
238         p.mOnClickListener = mRingtoneClickListener;
239         p.mLabelColumn = COLUMN_LABEL;
240         p.mIsSingleChoice = true;
241         p.mOnItemSelectedListener = this;
242         if (mShowOkCancelButtons) {
243             p.mPositiveButtonText = getString(com.android.internal.R.string.ok);
244             p.mPositiveButtonListener = this;
245             p.mNegativeButtonText = getString(com.android.internal.R.string.cancel);
246             p.mPositiveButtonListener = this;
247         }
248         p.mOnPrepareListViewListener = this;
249 
250         p.mTitle = intent.getCharSequenceExtra(RingtoneManager.EXTRA_RINGTONE_TITLE);
251         if (p.mTitle == null) {
252           if (mType == RingtoneManager.TYPE_ALARM) {
253               p.mTitle = getString(com.android.internal.R.string.ringtone_picker_title_alarm);
254           } else if (mType == RingtoneManager.TYPE_NOTIFICATION) {
255               p.mTitle =
256                   getString(com.android.internal.R.string.ringtone_picker_title_notification);
257           } else {
258               p.mTitle = getString(com.android.internal.R.string.ringtone_picker_title);
259           }
260         }
261 
262         setupAlert();
263     }
264     @Override
onSaveInstanceState(Bundle outState)265     public void onSaveInstanceState(Bundle outState) {
266         super.onSaveInstanceState(outState);
267         outState.putInt(SAVE_CLICKED_POS, getCheckedItem());
268     }
269 
270     @Override
onActivityResult(int requestCode, int resultCode, Intent data)271     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
272         super.onActivityResult(requestCode, resultCode, data);
273 
274         if (requestCode == ADD_FILE_REQUEST_CODE && resultCode == RESULT_OK) {
275             // Add the custom ringtone in a separate thread
276             final AsyncTask<Uri, Void, Uri> installTask = new AsyncTask<Uri, Void, Uri>() {
277                 @Override
278                 protected Uri doInBackground(Uri... params) {
279                     try {
280                         return mRingtoneManager.addCustomExternalRingtone(params[0], mType);
281                     } catch (IOException | IllegalArgumentException e) {
282                         Log.e(TAG, "Unable to add new ringtone", e);
283                     }
284                     return null;
285                 }
286 
287                 @Override
288                 protected void onPostExecute(Uri ringtoneUri) {
289                     if (ringtoneUri != null) {
290                         requeryForAdapter();
291                     } else {
292                         // Ringtone was not added, display error Toast
293                         Toast.makeText(RingtonePickerActivity.this, R.string.unable_to_add_ringtone,
294                                 Toast.LENGTH_SHORT).show();
295                     }
296                 }
297             };
298             installTask.execute(data.getData());
299         }
300     }
301 
302     // Disabled because context menus aren't Material Design :(
303     /*
304     @Override
305     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
306         int position = ((AdapterContextMenuInfo) menuInfo).position;
307 
308         Ringtone ringtone = getRingtone(getRingtoneManagerPosition(position));
309         if (ringtone != null && mRingtoneManager.isCustomRingtone(ringtone.getUri())) {
310             // It's a custom ringtone so we display the context menu
311             menu.setHeaderTitle(ringtone.getTitle(this));
312             menu.add(Menu.NONE, Menu.FIRST, Menu.NONE, R.string.delete_ringtone_text);
313         }
314     }
315 
316     @Override
317     public boolean onContextItemSelected(MenuItem item) {
318         switch (item.getItemId()) {
319             case Menu.FIRST: {
320                 int deletedRingtonePos = ((AdapterContextMenuInfo) item.getMenuInfo()).position;
321                 Uri deletedRingtoneUri = getRingtone(
322                         getRingtoneManagerPosition(deletedRingtonePos)).getUri();
323                 if(mRingtoneManager.deleteExternalRingtone(deletedRingtoneUri)) {
324                     requeryForAdapter();
325                 } else {
326                     Toast.makeText(this, R.string.unable_to_delete_ringtone, Toast.LENGTH_SHORT)
327                             .show();
328                 }
329                 return true;
330             }
331             default: {
332                 return false;
333             }
334         }
335     }
336     */
337 
338     @Override
onDestroy()339     public void onDestroy() {
340         if (mCursor != null) {
341             mCursor.close();
342             mCursor = null;
343         }
344         super.onDestroy();
345     }
346 
onPrepareListView(ListView listView)347     public void onPrepareListView(ListView listView) {
348         // Reset the static item count, as this method can be called multiple times
349         mStaticItemCount = 0;
350 
351         if (mHasDefaultItem) {
352             mDefaultRingtonePos = addDefaultRingtoneItem(listView);
353 
354             if (getCheckedItem() == POS_UNKNOWN && RingtoneManager.isDefault(mExistingUri)) {
355                 setCheckedItem(mDefaultRingtonePos);
356             }
357         }
358 
359         if (mHasSilentItem) {
360             mSilentPos = addSilentItem(listView);
361 
362             // The 'Silent' item should use a null Uri
363             if (getCheckedItem() == POS_UNKNOWN && mExistingUri == null) {
364                 setCheckedItem(mSilentPos);
365             }
366         }
367 
368         if (getCheckedItem() == POS_UNKNOWN) {
369             setCheckedItem(getListPosition(mRingtoneManager.getRingtonePosition(mExistingUri)));
370         }
371 
372         // In the buttonless (watch-only) version, preemptively set our result since we won't
373         // have another chance to do so before the activity closes.
374         if (!mShowOkCancelButtons) {
375             setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
376         }
377         // If external storage is available, add a button to install sounds from storage.
378         if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
379             addNewRingtoneItem(listView);
380         }
381 
382         // Enable context menu in ringtone items
383         registerForContextMenu(listView);
384     }
385 
386     /**
387      * Re-query RingtoneManager for the most recent set of installed ringtones. May move the
388      * selected item position to match the new position of the chosen sound.
389      *
390      * This should only need to happen after adding or removing a ringtone.
391      */
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 
addNewRingtoneItem(ListView listView)443     private void addNewRingtoneItem(ListView listView) {
444         listView.addFooterView(getLayoutInflater().inflate(R.layout.add_ringtone_item, listView,
445                 false /* attachToRoot */));
446     }
447 
initRingtoneManager()448     private void initRingtoneManager() {
449         // Reinstantiate the RingtoneManager. Cursor.requery() was deprecated and calling it
450         // causes unexpected behavior.
451         mRingtoneManager = new RingtoneManager(mTargetContext, /* includeParentRingtones */ true);
452         if (mType != -1) {
453             mRingtoneManager.setType(mType);
454         }
455         mCursor = new LocalizedCursor(mRingtoneManager.getCursor(), getResources(), COLUMN_LABEL);
456     }
457 
getRingtone(int ringtoneManagerPosition)458     private Ringtone getRingtone(int ringtoneManagerPosition) {
459         if (ringtoneManagerPosition < 0) {
460             return null;
461         }
462         return mRingtoneManager.getRingtone(ringtoneManagerPosition);
463     }
464 
getCheckedItem()465     private int getCheckedItem() {
466         return mAlertParams.mCheckedItem;
467     }
468 
setCheckedItem(int pos)469     private void setCheckedItem(int pos) {
470         mAlertParams.mCheckedItem = pos;
471         mCheckedItemId = mAdapter.getItemId(getRingtoneManagerPosition(pos));
472     }
473 
474     /*
475      * On click of Ok/Cancel buttons
476      */
onClick(DialogInterface dialog, int which)477     public void onClick(DialogInterface dialog, int which) {
478         boolean positiveResult = which == DialogInterface.BUTTON_POSITIVE;
479 
480         // Stop playing the previous ringtone
481         mRingtoneManager.stopPreviousRingtone();
482 
483         if (positiveResult) {
484             setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
485         } else {
486             setResult(RESULT_CANCELED);
487         }
488 
489         finish();
490     }
491 
492     /*
493      * On item selected via keys
494      */
onItemSelected(AdapterView parent, View view, int position, long id)495     public void onItemSelected(AdapterView parent, View view, int position, long id) {
496         playRingtone(position, DELAY_MS_SELECTION_PLAYED);
497 
498         // In the buttonless (watch-only) version, preemptively set our result since we won't
499         // have another chance to do so before the activity closes.
500         if (!mShowOkCancelButtons) {
501             setSuccessResultWithRingtone(getCurrentlySelectedRingtoneUri());
502         }
503     }
504 
onNothingSelected(AdapterView parent)505     public void onNothingSelected(AdapterView parent) {
506     }
507 
playRingtone(int position, int delayMs)508     private void playRingtone(int position, int delayMs) {
509         mHandler.removeCallbacks(this);
510         mSampleRingtonePos = position;
511         mHandler.postDelayed(this, delayMs);
512     }
513 
run()514     public void run() {
515         stopAnyPlayingRingtone();
516         if (mSampleRingtonePos == mSilentPos) {
517             return;
518         }
519 
520         Ringtone ringtone;
521         if (mSampleRingtonePos == mDefaultRingtonePos) {
522             if (mDefaultRingtone == null) {
523                 mDefaultRingtone = RingtoneManager.getRingtone(this, mUriForDefaultItem);
524             }
525            /*
526             * Stream type of mDefaultRingtone is not set explicitly here.
527             * It should be set in accordance with mRingtoneManager of this Activity.
528             */
529             if (mDefaultRingtone != null) {
530                 mDefaultRingtone.setStreamType(mRingtoneManager.inferStreamType());
531             }
532             ringtone = mDefaultRingtone;
533             mCurrentRingtone = null;
534         } else {
535             ringtone = mRingtoneManager.getRingtone(getRingtoneManagerPosition(mSampleRingtonePos));
536             mCurrentRingtone = ringtone;
537         }
538 
539         if (ringtone != null) {
540             if (mAttributesFlags != 0) {
541                 ringtone.setAudioAttributes(
542                         new AudioAttributes.Builder(ringtone.getAudioAttributes())
543                                 .setFlags(mAttributesFlags)
544                                 .build());
545             }
546             ringtone.play();
547         }
548     }
549 
550     @Override
onStop()551     protected void onStop() {
552         super.onStop();
553 
554         if (!isChangingConfigurations()) {
555             stopAnyPlayingRingtone();
556         } else {
557             saveAnyPlayingRingtone();
558         }
559     }
560 
561     @Override
onPause()562     protected void onPause() {
563         super.onPause();
564         if (!isChangingConfigurations()) {
565             stopAnyPlayingRingtone();
566         }
567     }
568 
setSuccessResultWithRingtone(Uri ringtoneUri)569     private void setSuccessResultWithRingtone(Uri ringtoneUri) {
570       setResult(RESULT_OK,
571           new Intent().putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, ringtoneUri));
572     }
573 
getCurrentlySelectedRingtoneUri()574     private Uri getCurrentlySelectedRingtoneUri() {
575       if (getCheckedItem() == mDefaultRingtonePos) {
576         // Use the default Uri that they originally gave us.
577         return mUriForDefaultItem;
578       } else if (getCheckedItem() == mSilentPos) {
579         // Use a null Uri for the 'Silent' item.
580         return null;
581       } else {
582         return mRingtoneManager.getRingtoneUri(getRingtoneManagerPosition(getCheckedItem()));
583       }
584     }
585 
saveAnyPlayingRingtone()586     private void saveAnyPlayingRingtone() {
587         if (mDefaultRingtone != null && mDefaultRingtone.isPlaying()) {
588             sPlayingRingtone = mDefaultRingtone;
589         } else if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) {
590             sPlayingRingtone = mCurrentRingtone;
591         }
592     }
593 
stopAnyPlayingRingtone()594     private void stopAnyPlayingRingtone() {
595         if (sPlayingRingtone != null && sPlayingRingtone.isPlaying()) {
596             sPlayingRingtone.stop();
597         }
598         sPlayingRingtone = null;
599 
600         if (mDefaultRingtone != null && mDefaultRingtone.isPlaying()) {
601             mDefaultRingtone.stop();
602         }
603 
604         if (mRingtoneManager != null) {
605             mRingtoneManager.stopPreviousRingtone();
606         }
607     }
608 
getRingtoneManagerPosition(int listPos)609     private int getRingtoneManagerPosition(int listPos) {
610         return listPos - mStaticItemCount;
611     }
612 
getListPosition(int ringtoneManagerPos)613     private int getListPosition(int ringtoneManagerPos) {
614 
615         // If the manager position is -1 (for not found), return that
616         if (ringtoneManagerPos < 0) return ringtoneManagerPos;
617 
618         return ringtoneManagerPos + mStaticItemCount;
619     }
620 
621     private static class LocalizedCursor extends CursorWrapper {
622 
623         final int mTitleIndex;
624         final Resources mResources;
625         String mNamePrefix;
626         final Pattern mSanitizePattern;
627 
LocalizedCursor(Cursor cursor, Resources resources, String columnLabel)628         LocalizedCursor(Cursor cursor, Resources resources, String columnLabel) {
629             super(cursor);
630             mTitleIndex = mCursor.getColumnIndex(columnLabel);
631             mResources = resources;
632             mSanitizePattern = Pattern.compile("[^a-zA-Z0-9]");
633             if (mTitleIndex == -1) {
634                 Log.e(TAG, "No index for column " + columnLabel);
635                 mNamePrefix = null;
636             } else {
637                 try {
638                     // Build the prefix for the name of the resource to look up
639                     // format is: "ResourcePackageName::ResourceTypeName/"
640                     // (the type name is expected to be "string" but let's not hardcode it).
641                     // Here we use an existing resource "notification_sound_default" which is
642                     // always expected to be found.
643                     mNamePrefix = String.format("%s:%s/%s",
644                             mResources.getResourcePackageName(R.string.notification_sound_default),
645                             mResources.getResourceTypeName(R.string.notification_sound_default),
646                             SOUND_NAME_RES_PREFIX);
647                 } catch (NotFoundException e) {
648                     mNamePrefix = null;
649                 }
650             }
651         }
652 
653         /**
654          * Process resource name to generate a valid resource name.
655          * @param input
656          * @return a non-null String
657          */
sanitize(String input)658         private String sanitize(String input) {
659             if (input == null) {
660                 return "";
661             }
662             return mSanitizePattern.matcher(input).replaceAll("_").toLowerCase();
663         }
664 
665         @Override
getString(int columnIndex)666         public String getString(int columnIndex) {
667             final String defaultName = mCursor.getString(columnIndex);
668             if ((columnIndex != mTitleIndex) || (mNamePrefix == null)) {
669                 return defaultName;
670             }
671             TypedValue value = new TypedValue();
672             try {
673                 // the name currently in the database is used to derive a name to match
674                 // against resource names in this package
675                 mResources.getValue(mNamePrefix + sanitize(defaultName), value, false);
676             } catch (NotFoundException e) {
677                 // no localized string, use the default string
678                 return defaultName;
679             }
680             if ((value != null) && (value.type == TypedValue.TYPE_STRING)) {
681                 Log.d(TAG, String.format("Replacing name %s with %s",
682                         defaultName, value.string.toString()));
683                 return value.string.toString();
684             } else {
685                 Log.e(TAG, "Invalid value when looking up localized name, using " + defaultName);
686                 return defaultName;
687             }
688         }
689     }
690 
691     private class BadgedRingtoneAdapter extends CursorAdapter {
692         private final boolean mIsManagedProfile;
693 
BadgedRingtoneAdapter(Context context, Cursor cursor, boolean isManagedProfile)694         public BadgedRingtoneAdapter(Context context, Cursor cursor, boolean isManagedProfile) {
695             super(context, cursor);
696             mIsManagedProfile = isManagedProfile;
697         }
698 
699         @Override
getItemId(int position)700         public long getItemId(int position) {
701             if (position < 0) {
702                 return position;
703             }
704             return super.getItemId(position);
705         }
706 
707         @Override
newView(Context context, Cursor cursor, ViewGroup parent)708         public View newView(Context context, Cursor cursor, ViewGroup parent) {
709             LayoutInflater inflater = LayoutInflater.from(context);
710             return inflater.inflate(R.layout.radio_with_work_badge, parent, false);
711         }
712 
713         @Override
bindView(View view, Context context, Cursor cursor)714         public void bindView(View view, Context context, Cursor cursor) {
715             // Set text as the title of the ringtone
716             ((TextView) view.findViewById(R.id.checked_text_view))
717                     .setText(cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX));
718 
719             boolean isWorkRingtone = false;
720             if (mIsManagedProfile) {
721                 /*
722                  * Display the work icon if the ringtone belongs to a work profile. We can tell that
723                  * a ringtone belongs to a work profile if the picker user is a managed profile, the
724                  * ringtone Uri is in external storage, and either the uri has no user id or has the
725                  * id of the picker user
726                  */
727                 Uri currentUri = mRingtoneManager.getRingtoneUri(cursor.getPosition());
728                 int uriUserId = ContentProvider.getUserIdFromUri(currentUri, mPickerUserId);
729                 Uri uriWithoutUserId = ContentProvider.getUriWithoutUserId(currentUri);
730 
731                 if (uriUserId == mPickerUserId && uriWithoutUserId.toString()
732                         .startsWith(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())) {
733                     isWorkRingtone = true;
734                 }
735             }
736 
737             ImageView workIcon = (ImageView) view.findViewById(R.id.work_icon);
738             if(isWorkRingtone) {
739                 workIcon.setImageDrawable(getPackageManager().getUserBadgeForDensityNoBackground(
740                         UserHandle.of(mPickerUserId), -1 /* density */));
741                 workIcon.setVisibility(View.VISIBLE);
742             } else {
743                 workIcon.setVisibility(View.GONE);
744             }
745         }
746     }
747 }
748