| /* |
| * Copyright (C) 2021 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.car.settings.sound; |
| |
| import android.car.drivingstate.CarUxRestrictions; |
| import android.content.Context; |
| import android.content.pm.PackageManager; |
| import android.content.res.Resources; |
| import android.database.Cursor; |
| import android.database.CursorWrapper; |
| import android.media.AudioAttributes; |
| import android.media.Ringtone; |
| import android.media.RingtoneManager; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.UserHandle; |
| import android.provider.MediaStore; |
| import android.util.TypedValue; |
| import android.view.View; |
| |
| import androidx.annotation.VisibleForTesting; |
| import androidx.preference.PreferenceGroup; |
| import androidx.preference.TwoStatePreference; |
| |
| import com.android.car.settings.R; |
| import com.android.car.settings.common.FragmentController; |
| import com.android.car.settings.common.Logger; |
| import com.android.car.settings.common.PreferenceController; |
| import com.android.car.ui.preference.CarUiRadioButtonPreference; |
| |
| import java.util.regex.Pattern; |
| |
| /** A {@link PreferenceController} to help pick a default ringtone. */ |
| public class RingtonePickerPreferenceController extends PreferenceController<PreferenceGroup> { |
| |
| private static final Logger LOG = new Logger(RingtonePickerPreferenceController.class); |
| private static final String SOUND_NAME_RES_PREFIX = "sound_name_"; |
| |
| @VisibleForTesting |
| static final String COLUMN_LABEL = MediaStore.Audio.Media.TITLE; |
| |
| @VisibleForTesting |
| static final int SILENT_ITEM_POS = -1; |
| private static final int UNKNOWN_POS = -2; |
| |
| private final Context mUserContext; |
| private RingtoneManager mRingtoneManager; |
| private LocalizedCursor mCursor; |
| private Handler mHandler; |
| |
| /** See {@link RingtoneManager} for valid values. */ |
| private int mRingtoneType; |
| private boolean mHasSilentItem; |
| |
| private int mCurrentlySelectedPos = UNKNOWN_POS; |
| private TwoStatePreference mCurrentlySelectedPreference; |
| |
| private Ringtone mCurrentRingtone; |
| private int mAttributesFlags = 0; |
| private Bundle mArgs; |
| |
| public RingtonePickerPreferenceController(Context context, String preferenceKey, |
| FragmentController fragmentController, CarUxRestrictions uxRestrictions) { |
| super(context, preferenceKey, fragmentController, uxRestrictions); |
| |
| mUserContext = createPackageContextAsUser(getContext(), UserHandle.myUserId()); |
| mRingtoneManager = new RingtoneManager(getContext(), /* includeParentRingtones= */ true); |
| mHandler = new Handler(Looper.getMainLooper()); |
| mArgs = new Bundle(); |
| } |
| |
| /** Arguments used to configure this preference controller. */ |
| public void setArguments(Bundle args) { |
| mArgs = args; |
| } |
| |
| /** |
| * Returns the position of the currently checked preference. Returns 0 if no such element |
| * exists. |
| */ |
| public int getCurrentlySelectedPreferencePos() { |
| int count = getPreference().getPreferenceCount(); |
| for (int i = 0; i < count; i++) { |
| TwoStatePreference pref = (TwoStatePreference) getPreference().getPreference(i); |
| if (pref.isChecked()) { |
| return i; |
| } |
| } |
| return 0; |
| } |
| |
| /** Saves the currently selected ringtone. */ |
| public void saveRingtone() { |
| RingtoneManager.setActualDefaultRingtoneUri(mUserContext, mRingtoneType, |
| getCurrentlySelectedRingtoneUri()); |
| } |
| |
| @Override |
| protected Class<PreferenceGroup> getPreferenceType() { |
| return PreferenceGroup.class; |
| } |
| |
| @Override |
| protected void onCreateInternal() { |
| mRingtoneType = mArgs.getInt(RingtoneManager.EXTRA_RINGTONE_TYPE, /* defaultValue= */ -1); |
| mHasSilentItem = mArgs.getBoolean(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, |
| /* defaultValue= */ true); |
| mAttributesFlags |= mArgs.getInt(RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS, |
| /* defaultValue= */ 0); |
| |
| mRingtoneManager.setType(mRingtoneType); |
| mCursor = new LocalizedCursor(mRingtoneManager.getCursor(), getContext().getResources(), |
| COLUMN_LABEL); |
| } |
| |
| @Override |
| protected void onStartInternal() { |
| populateRingtones(getPreference()); |
| |
| clearSelection(); |
| Uri currentRingtoneUri = |
| RingtoneManager.getActualDefaultRingtoneUri(mUserContext, mRingtoneType); |
| initSelection(currentRingtoneUri); |
| } |
| |
| @Override |
| protected void onStopInternal() { |
| stopAnyPlayingRingtone(); |
| clearSelection(); |
| } |
| |
| private void populateRingtones(PreferenceGroup preference) { |
| preference.removeAll(); |
| |
| // Keep at the front of the list, if requested. |
| if (mHasSilentItem) { |
| String label = getContext().getResources().getString( |
| com.android.internal.R.string.ringtone_silent); |
| int pos = SILENT_ITEM_POS; |
| preference.addPreference(createRingtonePreference(label, pos)); |
| } |
| |
| int pos = 0; |
| mCursor.moveToFirst(); |
| while (!mCursor.isAfterLast()) { |
| String label = mCursor.getString(mCursor.getColumnIndex(COLUMN_LABEL)); |
| preference.addPreference(createRingtonePreference(label, pos)); |
| |
| mCursor.moveToNext(); |
| pos++; |
| } |
| } |
| |
| private TwoStatePreference createRingtonePreference(String title, int key) { |
| CarUiRadioButtonPreference preference = new CarUiRadioButtonPreference(getContext()); |
| preference.setTitle(title); |
| preference.setKey(Integer.toString(key)); |
| preference.setChecked(false); |
| preference.setViewId(View.NO_ID); |
| preference.setOnPreferenceClickListener(pref -> { |
| updateCurrentSelection((TwoStatePreference) pref); |
| playCurrentlySelectedRingtone(); |
| return true; |
| }); |
| return preference; |
| } |
| |
| private void updateCurrentSelection(TwoStatePreference preference) { |
| int selectedPos = Integer.parseInt(preference.getKey()); |
| if (mCurrentlySelectedPos != selectedPos) { |
| if (mCurrentlySelectedPreference != null) { |
| mCurrentlySelectedPreference.setChecked(false); |
| mCurrentlySelectedPreference.setViewId(View.NO_ID); |
| } |
| } |
| mCurrentlySelectedPreference = preference; |
| mCurrentlySelectedPos = selectedPos; |
| mCurrentlySelectedPreference.setChecked(true); |
| mCurrentlySelectedPreference.setViewId(R.id.ringtone_picker_selected_id); |
| } |
| |
| private void initSelection(Uri uri) { |
| if (uri == null) { |
| mCurrentlySelectedPos = SILENT_ITEM_POS; |
| } else { |
| mCurrentlySelectedPos = mRingtoneManager.getRingtonePosition(uri); |
| } |
| int count = getPreference().getPreferenceCount(); |
| for (int i = 0; i < count; i++) { |
| TwoStatePreference pref = (TwoStatePreference) getPreference().getPreference(i); |
| int pos = Integer.parseInt(pref.getKey()); |
| if (mCurrentlySelectedPos == pos) { |
| mCurrentlySelectedPreference = pref; |
| pref.setChecked(true); |
| pref.setViewId(R.id.ringtone_picker_selected_id); |
| } |
| } |
| } |
| |
| private void clearSelection() { |
| int count = getPreference().getPreferenceCount(); |
| for (int i = 0; i < count; i++) { |
| TwoStatePreference pref = (TwoStatePreference) getPreference().getPreference(i); |
| pref.setChecked(false); |
| pref.setViewId(View.NO_ID); |
| } |
| |
| mCurrentlySelectedPreference = null; |
| mCurrentlySelectedPos = UNKNOWN_POS; |
| } |
| |
| private void playCurrentlySelectedRingtone() { |
| mHandler.removeCallbacks(this::run); |
| mHandler.post(this::run); |
| } |
| |
| private void run() { |
| stopAnyPlayingRingtone(); |
| if (mCurrentlySelectedPos == SILENT_ITEM_POS) { |
| return; |
| } |
| |
| if (mCurrentlySelectedPos >= 0) { |
| mCurrentRingtone = mRingtoneManager.getRingtone(mCurrentlySelectedPos); |
| } |
| |
| if (mCurrentRingtone != null) { |
| if (mAttributesFlags != 0) { |
| mCurrentRingtone.setAudioAttributes( |
| new AudioAttributes.Builder(mCurrentRingtone.getAudioAttributes()) |
| .setFlags(mAttributesFlags) |
| .build()); |
| } |
| mCurrentRingtone.play(); |
| } |
| } |
| |
| private void stopAnyPlayingRingtone() { |
| mHandler.removeCallbacks(this::run); |
| |
| if (mCurrentRingtone != null && mCurrentRingtone.isPlaying()) { |
| mCurrentRingtone.stop(); |
| } |
| |
| if (mRingtoneManager != null) { |
| mRingtoneManager.stopPreviousRingtone(); |
| } |
| } |
| |
| private Uri getCurrentlySelectedRingtoneUri() { |
| if (mCurrentlySelectedPos >= 0) { |
| return mRingtoneManager.getRingtoneUri(mCurrentlySelectedPos); |
| } else if (mCurrentlySelectedPos == SILENT_ITEM_POS) { |
| // Use a null Uri for the 'Silent' item. |
| return null; |
| } else { |
| LOG.e("Requesting ringtone URI for unknown position: " + mCurrentlySelectedPos); |
| return null; |
| } |
| } |
| |
| /** |
| * Returns a context created from the given context for the given user, or null if it fails. |
| */ |
| private Context createPackageContextAsUser(Context context, int userId) { |
| try { |
| return context.createPackageContextAsUser( |
| context.getPackageName(), /* flags= */ 0, UserHandle.of(userId)); |
| } catch (PackageManager.NameNotFoundException e) { |
| LOG.e("Failed to create user context", e); |
| } |
| return null; |
| } |
| |
| /** |
| * A copy of the localized cursor provided in |
| * {@link com.android.soundpicker.RingtonePickerActivity}. |
| */ |
| private static class LocalizedCursor extends CursorWrapper { |
| |
| final int mTitleIndex; |
| final Resources mResources; |
| final Pattern mSanitizePattern; |
| String mNamePrefix; |
| |
| LocalizedCursor(Cursor cursor, Resources resources, String columnLabel) { |
| super(cursor); |
| mTitleIndex = mCursor.getColumnIndex(columnLabel); |
| mResources = resources; |
| mSanitizePattern = Pattern.compile("[^a-zA-Z0-9]"); |
| if (mTitleIndex == -1) { |
| LOG.e("No index for column " + columnLabel); |
| mNamePrefix = null; |
| } else { |
| try { |
| // Build the prefix for the name of the resource to look up. |
| // Format is: "ResourcePackageName::ResourceTypeName/" |
| // (The type name is expected to be "string" but let's not hardcode it). |
| // Here we use an existing resource "ringtone_title" which is |
| // always expected to be found. |
| mNamePrefix = String.format("%s:%s/%s", |
| mResources.getResourcePackageName(R.string.ringtone_title), |
| mResources.getResourceTypeName(R.string.ringtone_title), |
| SOUND_NAME_RES_PREFIX); |
| } catch (Resources.NotFoundException e) { |
| mNamePrefix = null; |
| } |
| } |
| } |
| |
| /** |
| * Process resource name to generate a valid resource name. |
| * |
| * @return a non-null String |
| */ |
| private String sanitize(String input) { |
| if (input == null) { |
| return ""; |
| } |
| return mSanitizePattern.matcher(input).replaceAll("_").toLowerCase(); |
| } |
| |
| @Override |
| public String getString(int columnIndex) { |
| final String defaultName = mCursor.getString(columnIndex); |
| if ((columnIndex != mTitleIndex) || (mNamePrefix == null)) { |
| return defaultName; |
| } |
| TypedValue value = new TypedValue(); |
| try { |
| // The name currently in the database is used to derive a name to match |
| // against resource names in this package. |
| mResources.getValue(mNamePrefix + sanitize(defaultName), value, false); |
| } catch (Resources.NotFoundException e) { |
| // No localized string, use the default string. |
| return defaultName; |
| } |
| if ((value != null) && (value.type == TypedValue.TYPE_STRING)) { |
| LOG.d(String.format("Replacing name %s with %s", |
| defaultName, value.string.toString())); |
| return value.string.toString(); |
| } else { |
| LOG.e("Invalid value when looking up localized name, using " + defaultName); |
| return defaultName; |
| } |
| } |
| } |
| |
| @VisibleForTesting |
| void setRingtoneManager(RingtoneManager ringtoneManager) { |
| mRingtoneManager = ringtoneManager; |
| } |
| } |