RingtonePickerActivity.java revision 385f2371687760c040e734840ef57de5d7451e8d
1bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant/*
2bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant * Copyright (C) 2007 The Android Open Source Project
3f5256e16dfc425c1d466f6308d4026d529ce9e0bHoward Hinnant *
4bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant * Licensed under the Apache License, Version 2.0 (the "License");
5b64f8b07c104c6cc986570ac8ee0ed16a9f23976Howard Hinnant * you may not use this file except in compliance with the License.
6b64f8b07c104c6cc986570ac8ee0ed16a9f23976Howard Hinnant * You may obtain a copy of the License at
7bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant *
8bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant *      http://www.apache.org/licenses/LICENSE-2.0
9bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant *
10bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant * Unless required by applicable law or agreed to in writing, software
11bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant * distributed under the License is distributed on an "AS IS" BASIS,
12bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant * See the License for the specific language governing permissions and
1418dbed95969596840835876627ecd102b4fc51e1Eric Fiselier * limitations under the License.
1518dbed95969596840835876627ecd102b4fc51e1Eric Fiselier */
16bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
17bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantpackage com.android.providers.media;
1898605940df7a54649618c541b972a308cccaade9Stephan T. Lavavej
19bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.content.DialogInterface;
20bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.content.Intent;
21061d0cc4db18d17bf01ed14c5db0be098205bd47Marshall Clowimport android.content.res.Resources;
22bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.content.res.Resources.NotFoundException;
23bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.database.Cursor;
24fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnantimport android.database.CursorWrapper;
25fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnantimport android.media.AudioAttributes;
26bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.media.Ringtone;
27bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.media.RingtoneManager;
28bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.net.Uri;
29bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.os.Bundle;
30bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.os.Handler;
31bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.provider.MediaStore;
32bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.provider.Settings;
33bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.util.Log;
34bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.util.TypedValue;
35bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.view.View;
36fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnantimport android.widget.AdapterView;
37bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.widget.ListView;
38bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport android.widget.TextView;
39bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
40bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport com.android.internal.app.AlertActivity;
41bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport com.android.internal.app.AlertController;
42bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
43bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantimport java.util.Objects;
445495e2efb9ea9fcb273ebed2f92b912ace28e82bEric Fiselierimport java.util.regex.Pattern;
45bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
46fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant/**
47bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant * The {@link RingtonePickerActivity} allows the user to choose one from all of the
48fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant * available ringtones. The chosen ringtone's URI will be persisted as a string.
49bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant *
50fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant * @see RingtoneManager#ACTION_RINGTONE_PICKER
51bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant */
52bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnantpublic final class RingtonePickerActivity extends AlertActivity implements
53bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant        AdapterView.OnItemSelectedListener, Runnable, DialogInterface.OnClickListener,
54bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant        AlertController.AlertParams.OnPrepareListViewListener {
5598605940df7a54649618c541b972a308cccaade9Stephan T. Lavavej
56bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private static final int POS_UNKNOWN = -1;
57bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
58bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private static final String TAG = "RingtonePickerActivity";
59fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant
60bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private static final int DELAY_MS_SELECTION_PLAYED = 300;
61bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
62bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private static final String COLUMN_LABEL = MediaStore.Audio.Media.TITLE;
63bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
64bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private static final String SAVE_CLICKED_POS = "clicked_pos";
65bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
66bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private static final String SOUND_NAME_RES_PREFIX = "sound_name_";
67fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant
68bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private RingtoneManager mRingtoneManager;
69bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private int mType;
70bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
71bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private Cursor mCursor;
72bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private Handler mHandler;
73bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
74bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    /** The position in the list of the 'Silent' item. */
75fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant    private int mSilentPos = POS_UNKNOWN;
76bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
77bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    /** The position in the list of the 'Default' item. */
78bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private int mDefaultRingtonePos = POS_UNKNOWN;
79bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
80bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    /** The position in the list of the last clicked item. */
81bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private int mClickedPos = POS_UNKNOWN;
82bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
83fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant    /** The position in the list of the ringtone to sample. */
84bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private int mSampleRingtonePos = POS_UNKNOWN;
85bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
86bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    /** Whether this list has the 'Silent' item. */
87bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private boolean mHasSilentItem;
88bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
89bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    /** The Uri to place a checkmark next to. */
90bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private Uri mExistingUri;
91bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
92fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant    /** The number of static items in the list. */
93fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant    private int mStaticItemCount;
94fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant
95fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant    /** Whether this list has the 'Default' item. */
96fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant    private boolean mHasDefaultItem;
97fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant
98fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant    /** The Uri to play when the 'Default' item is clicked. */
99fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant    private Uri mUriForDefaultItem;
100bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant
101bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    /**
102bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant     * A Ringtone for the default ringtone. In most cases, the RingtoneManager
103bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant     * will stop the previous ringtone. However, the RingtoneManager doesn't
104fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant     * manage the default ringtone for us, so we should stop this one manually.
105fcd8db7133c56a5a627f3922ce4a180c12287dd9Howard Hinnant     */
106bc8d3f97eb5c958007f2713238472e0c1c8fe02Howard Hinnant    private Ringtone mDefaultRingtone;
107
108    /**
109     * The ringtone that's currently playing, unless the currently playing one is the default
110     * ringtone.
111     */
112    private Ringtone mCurrentRingtone;
113
114    private int mAttributesFlags;
115
116    private boolean mShowOkCancelButtons;
117
118    /**
119     * Keep the currently playing ringtone around when changing orientation, so that it
120     * can be stopped later, after the activity is recreated.
121     */
122    private static Ringtone sPlayingRingtone;
123
124    private DialogInterface.OnClickListener mRingtoneClickListener =
125            new DialogInterface.OnClickListener() {
126
127        /*
128         * On item clicked
129         */
130        public void onClick(DialogInterface dialog, int which) {
131            // Save the position of most recently clicked item
132            mClickedPos = which;
133
134            // In the buttonless (watch-only) version, preemptively set our result since we won't
135            // have another chance to do so before the activity closes.
136            if (!mShowOkCancelButtons) {
137                setResultFromSelection();
138            }
139
140            // Play clip
141            playRingtone(which, 0);
142        }
143
144    };
145
146    @Override
147    protected void onCreate(Bundle savedInstanceState) {
148        super.onCreate(savedInstanceState);
149
150        mHandler = new Handler();
151
152        Intent intent = getIntent();
153
154        // Give the Activity so it can do managed queries
155        mRingtoneManager = new RingtoneManager(this);
156
157        // Get the types of ringtones to show
158        mType = intent.getIntExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, -1);
159        if (mType != -1) {
160            mRingtoneManager.setType(mType);
161        }
162
163        /*
164         * Get whether to show the 'Default' item, and the URI to play when the
165         * default is clicked
166         */
167        mHasDefaultItem = intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true);
168        mUriForDefaultItem = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI);
169        if (mUriForDefaultItem == null) {
170            if (mType == RingtoneManager.TYPE_NOTIFICATION) {
171                mUriForDefaultItem = Settings.System.DEFAULT_NOTIFICATION_URI;
172            } else if (mType == RingtoneManager.TYPE_ALARM) {
173                mUriForDefaultItem = Settings.System.DEFAULT_ALARM_ALERT_URI;
174            } else if (mType == RingtoneManager.TYPE_RINGTONE) {
175                mUriForDefaultItem = Settings.System.DEFAULT_RINGTONE_URI;
176            } else {
177                // or leave it null for silence.
178                mUriForDefaultItem = Settings.System.DEFAULT_RINGTONE_URI;
179            }
180        }
181
182        if (savedInstanceState != null) {
183            mClickedPos = savedInstanceState.getInt(SAVE_CLICKED_POS, POS_UNKNOWN);
184        }
185        // Get whether to show the 'Silent' item
186        mHasSilentItem = intent.getBooleanExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true);
187        // AudioAttributes flags
188        mAttributesFlags |= intent.getIntExtra(
189                RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS,
190                0 /*defaultValue == no flags*/);
191
192        mShowOkCancelButtons = getResources().getBoolean(R.bool.config_showOkCancelButtons);
193
194
195        mCursor = new LocalizedCursor(mRingtoneManager.getCursor(), getResources(), COLUMN_LABEL);
196
197        // The volume keys will control the stream that we are choosing a ringtone for
198        setVolumeControlStream(mRingtoneManager.inferStreamType());
199
200        // Get the URI whose list item should have a checkmark
201        mExistingUri = intent
202                .getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI);
203
204        final AlertController.AlertParams p = mAlertParams;
205        p.mCursor = mCursor;
206        p.mOnClickListener = mRingtoneClickListener;
207        p.mLabelColumn = COLUMN_LABEL;
208        p.mIsSingleChoice = true;
209        p.mOnItemSelectedListener = this;
210        if (mShowOkCancelButtons) {
211            p.mPositiveButtonText = getString(com.android.internal.R.string.ok);
212            p.mPositiveButtonListener = this;
213            p.mNegativeButtonText = getString(com.android.internal.R.string.cancel);
214            p.mPositiveButtonListener = this;
215        }
216        p.mOnPrepareListViewListener = this;
217
218        p.mTitle = intent.getCharSequenceExtra(RingtoneManager.EXTRA_RINGTONE_TITLE);
219        if (p.mTitle == null) {
220            p.mTitle = getString(com.android.internal.R.string.ringtone_picker_title);
221        }
222
223        setupAlert();
224    }
225    @Override
226    public void onSaveInstanceState(Bundle outState) {
227        super.onSaveInstanceState(outState);
228        outState.putInt(SAVE_CLICKED_POS, mClickedPos);
229    }
230
231    public void onPrepareListView(ListView listView) {
232
233        if (mHasDefaultItem) {
234            mDefaultRingtonePos = addDefaultRingtoneItem(listView);
235
236            if (mClickedPos == POS_UNKNOWN && RingtoneManager.isDefault(mExistingUri)) {
237                mClickedPos = mDefaultRingtonePos;
238            }
239        }
240
241        if (mHasSilentItem) {
242            mSilentPos = addSilentItem(listView);
243
244            // The 'Silent' item should use a null Uri
245            if (mClickedPos == POS_UNKNOWN && mExistingUri == null) {
246                mClickedPos = mSilentPos;
247            }
248        }
249
250        if (mClickedPos == POS_UNKNOWN) {
251            mClickedPos = getListPosition(mRingtoneManager.getRingtonePosition(mExistingUri));
252        }
253
254        // In the buttonless (watch-only) version, preemptively set our result since we won't
255        // have another chance to do so before the activity closes.
256        if (!mShowOkCancelButtons) {
257            setResultFromSelection();
258        }
259        // Put a checkmark next to an item.
260        mAlertParams.mCheckedItem = mClickedPos;
261    }
262
263    /**
264     * Adds a static item to the top of the list. A static item is one that is not from the
265     * RingtoneManager.
266     *
267     * @param listView The ListView to add to.
268     * @param textResId The resource ID of the text for the item.
269     * @return The position of the inserted item.
270     */
271    private int addStaticItem(ListView listView, int textResId) {
272        TextView textView = (TextView) getLayoutInflater().inflate(
273                com.android.internal.R.layout.select_dialog_singlechoice_material, listView, false);
274        textView.setText(textResId);
275        listView.addHeaderView(textView);
276        mStaticItemCount++;
277        return listView.getHeaderViewsCount() - 1;
278    }
279
280    private int addDefaultRingtoneItem(ListView listView) {
281        if (mType == RingtoneManager.TYPE_NOTIFICATION) {
282            return addStaticItem(listView, R.string.notification_sound_default);
283        } else if (mType == RingtoneManager.TYPE_ALARM) {
284            return addStaticItem(listView, R.string.alarm_sound_default);
285        }
286
287        return addStaticItem(listView, R.string.ringtone_default);
288    }
289
290    private int addSilentItem(ListView listView) {
291        return addStaticItem(listView, com.android.internal.R.string.ringtone_silent);
292    }
293
294    /*
295     * On click of Ok/Cancel buttons
296     */
297    public void onClick(DialogInterface dialog, int which) {
298        boolean positiveResult = which == DialogInterface.BUTTON_POSITIVE;
299
300        // Stop playing the previous ringtone
301        mRingtoneManager.stopPreviousRingtone();
302
303        if (positiveResult) {
304            setResultFromSelection();
305        } else {
306            setResult(RESULT_CANCELED);
307        }
308
309        finish();
310    }
311
312    /*
313     * On item selected via keys
314     */
315    public void onItemSelected(AdapterView parent, View view, int position, long id) {
316        mClickedPos = position;
317        playRingtone(position, DELAY_MS_SELECTION_PLAYED);
318
319        // In the buttonless (watch-only) version, preemptively set our result since we won't
320        // have another chance to do so before the activity closes.
321        if (!mShowOkCancelButtons) {
322            setResultFromSelection();
323        }
324    }
325
326    public void onNothingSelected(AdapterView parent) {
327    }
328
329    private void playRingtone(int position, int delayMs) {
330        mHandler.removeCallbacks(this);
331        mSampleRingtonePos = position;
332        mHandler.postDelayed(this, delayMs);
333    }
334
335    public void run() {
336        stopAnyPlayingRingtone();
337        if (mSampleRingtonePos == mSilentPos) {
338            return;
339        }
340
341        Ringtone ringtone;
342        if (mSampleRingtonePos == mDefaultRingtonePos) {
343            if (mDefaultRingtone == null) {
344                mDefaultRingtone = RingtoneManager.getRingtone(this, mUriForDefaultItem);
345            }
346           /*
347            * Stream type of mDefaultRingtone is not set explicitly here.
348            * It should be set in accordance with mRingtoneManager of this Activity.
349            */
350            if (mDefaultRingtone != null) {
351                mDefaultRingtone.setStreamType(mRingtoneManager.inferStreamType());
352            }
353            ringtone = mDefaultRingtone;
354            mCurrentRingtone = null;
355        } else {
356            ringtone = mRingtoneManager.getRingtone(getRingtoneManagerPosition(mSampleRingtonePos));
357            mCurrentRingtone = ringtone;
358        }
359
360        if (ringtone != null) {
361            if (mAttributesFlags != 0) {
362                ringtone.setAudioAttributes(
363                        new AudioAttributes.Builder(ringtone.getAudioAttributes())
364                                .setFlags(mAttributesFlags)
365                                .build());
366            }
367            ringtone.play();
368        }
369    }
370
371    @Override
372    protected void onStop() {
373        super.onStop();
374        mCursor.deactivate();
375
376        if (!isChangingConfigurations()) {
377            stopAnyPlayingRingtone();
378        } else {
379            saveAnyPlayingRingtone();
380        }
381    }
382
383    @Override
384    protected void onPause() {
385        super.onPause();
386        if (!isChangingConfigurations()) {
387            stopAnyPlayingRingtone();
388        }
389    }
390
391    private void setResultFromSelection() {
392        // Obtain the currently selected ringtone
393        Uri uri = null;
394        if (mClickedPos == mDefaultRingtonePos) {
395            // Set it to the default Uri that they originally gave us
396            uri = mUriForDefaultItem;
397        } else if (mClickedPos == mSilentPos) {
398            // A null Uri is for the 'Silent' item
399            uri = null;
400        } else {
401            uri = mRingtoneManager.getRingtoneUri(getRingtoneManagerPosition(mClickedPos));
402        }
403
404        // Return new URI if another ringtone was selected, as there's no ok/cancel button
405        if (Objects.equals(uri, mExistingUri)) {
406            setResult(RESULT_CANCELED);
407        } else {
408            Intent resultIntent = new Intent();
409            resultIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI, uri);
410            setResult(RESULT_OK, resultIntent);
411        }
412    }
413
414    private void saveAnyPlayingRingtone() {
415        if (mDefaultRingtone != null && mDefaultRingtone.isPlaying()) {
416            sPlayingRingtone = mDefaultRingtone;
417        } else if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) {
418            sPlayingRingtone = mCurrentRingtone;
419        }
420    }
421
422    private void stopAnyPlayingRingtone() {
423        if (sPlayingRingtone != null && sPlayingRingtone.isPlaying()) {
424            sPlayingRingtone.stop();
425        }
426        sPlayingRingtone = null;
427
428        if (mDefaultRingtone != null && mDefaultRingtone.isPlaying()) {
429            mDefaultRingtone.stop();
430        }
431
432        if (mRingtoneManager != null) {
433            mRingtoneManager.stopPreviousRingtone();
434        }
435    }
436
437    private int getRingtoneManagerPosition(int listPos) {
438        return listPos - mStaticItemCount;
439    }
440
441    private int getListPosition(int ringtoneManagerPos) {
442
443        // If the manager position is -1 (for not found), return that
444        if (ringtoneManagerPos < 0) return ringtoneManagerPos;
445
446        return ringtoneManagerPos + mStaticItemCount;
447    }
448
449    private static class LocalizedCursor extends CursorWrapper {
450
451        final int mTitleIndex;
452        final Resources mResources;
453        String mNamePrefix;
454        final Pattern mSanitizePattern;
455
456        LocalizedCursor(Cursor cursor, Resources resources, String columnLabel) {
457            super(cursor);
458            mTitleIndex = mCursor.getColumnIndex(columnLabel);
459            mResources = resources;
460            mSanitizePattern = Pattern.compile("[^a-zA-Z0-9]");
461            if (mTitleIndex == -1) {
462                Log.e(TAG, "No index for column " + columnLabel);
463                mNamePrefix = null;
464            } else {
465                try {
466                    // Build the prefix for the name of the resource to look up
467                    // format is: "ResourcePackageName::ResourceTypeName/"
468                    // (the type name is expected to be "string" but let's not hardcode it).
469                    // Here we use an existing resource "notification_sound_default" which is
470                    // always expected to be found.
471                    mNamePrefix = String.format("%s:%s/%s",
472                            mResources.getResourcePackageName(R.string.notification_sound_default),
473                            mResources.getResourceTypeName(R.string.notification_sound_default),
474                            SOUND_NAME_RES_PREFIX);
475                } catch (NotFoundException e) {
476                    mNamePrefix = null;
477                }
478            }
479        }
480
481        /**
482         * Process resource name to generate a valid resource name.
483         * @param input
484         * @return a non-null String
485         */
486        private String sanitize(String input) {
487            if (input == null) {
488                return "";
489            }
490            return mSanitizePattern.matcher(input).replaceAll("_").toLowerCase();
491        }
492
493        @Override
494        public String getString(int columnIndex) {
495            final String defaultName = mCursor.getString(columnIndex);
496            if ((columnIndex != mTitleIndex) || (mNamePrefix == null)) {
497                return defaultName;
498            }
499            TypedValue value = new TypedValue();
500            try {
501                // the name currently in the database is used to derive a name to match
502                // against resource names in this package
503                mResources.getValue(mNamePrefix + sanitize(defaultName), value, false);
504            } catch (NotFoundException e) {
505                // no localized string, use the default string
506                return defaultName;
507            }
508            if ((value != null) && (value.type == TypedValue.TYPE_STRING)) {
509                Log.d(TAG, String.format("Replacing name %s with %s",
510                        defaultName, value.string.toString()));
511                return value.string.toString();
512            } else {
513                Log.e(TAG, "Invalid value when looking up localized name, using " + defaultName);
514                return defaultName;
515            }
516        }
517    }
518}
519