blob: 27caff53061ce82650bbf4d798660a531b05e881 [file] [log] [blame]
/*
* 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;
}
}