| /* | |
| * Copyright (C) 2007 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.settings; | |
| import android.content.ContentProvider; | |
| import android.content.ContentResolver; | |
| import android.content.Context; | |
| import android.content.Intent; | |
| import android.content.res.TypedArray; | |
| import android.media.AudioAttributes; | |
| import android.media.RingtoneManager; | |
| import android.net.Uri; | |
| import android.os.UserHandle; | |
| import android.os.UserManager; | |
| import android.provider.Settings.System; | |
| import android.text.TextUtils; | |
| import android.util.AttributeSet; | |
| import android.util.Log; | |
| import androidx.preference.Preference; | |
| import androidx.preference.PreferenceManager; | |
| /** | |
| * A {@link Preference} that allows the user to choose a ringtone from those on the device. | |
| * The chosen ringtone's URI will be persisted as a string. | |
| * <p> | |
| * If the user chooses the "Default" item, the saved string will be one of | |
| * {@link System#DEFAULT_RINGTONE_URI}, | |
| * {@link System#DEFAULT_NOTIFICATION_URI}, or | |
| * {@link System#DEFAULT_ALARM_ALERT_URI}. If the user chooses the "Silent" | |
| * item, the saved string will be an empty string. | |
| * | |
| * @attr ref android.R.styleable#RingtonePreference_ringtoneType | |
| * @attr ref android.R.styleable#RingtonePreference_showDefault | |
| * @attr ref android.R.styleable#RingtonePreference_showSilent | |
| * | |
| * Based of frameworks/base/core/java/android/preference/RingtonePreference.java | |
| * but extends androidx.preference.Preference instead. | |
| */ | |
| public class RingtonePreference extends Preference { | |
| private static final String TAG = "RingtonePreference"; | |
| private int mRingtoneType; | |
| private boolean mShowDefault; | |
| private boolean mShowSilent; | |
| private int mRequestCode; | |
| protected int mUserId; | |
| protected Context mUserContext; | |
| public RingtonePreference(Context context, AttributeSet attrs) { | |
| super(context, attrs); | |
| final TypedArray a = context.obtainStyledAttributes(attrs, | |
| com.android.internal.R.styleable.RingtonePreference, 0, 0); | |
| mRingtoneType = a.getInt(com.android.internal.R.styleable.RingtonePreference_ringtoneType, | |
| RingtoneManager.TYPE_RINGTONE); | |
| mShowDefault = a.getBoolean(com.android.internal.R.styleable.RingtonePreference_showDefault, | |
| true); | |
| mShowSilent = a.getBoolean(com.android.internal.R.styleable.RingtonePreference_showSilent, | |
| true); | |
| setIntent(new Intent(RingtoneManager.ACTION_RINGTONE_PICKER) | |
| .setPackage(context.getString(R.string.config_sound_picker_package_name))); | |
| setUserId(UserHandle.myUserId()); | |
| a.recycle(); | |
| } | |
| public void setUserId(int userId) { | |
| mUserId = userId; | |
| mUserContext = Utils.createPackageContextAsUser(getContext(), mUserId); | |
| } | |
| public int getUserId() { | |
| return mUserId; | |
| } | |
| /** | |
| * Returns the sound type(s) that are shown in the picker. | |
| * | |
| * @return The sound type(s) that are shown in the picker. | |
| * @see #setRingtoneType(int) | |
| */ | |
| public int getRingtoneType() { | |
| return mRingtoneType; | |
| } | |
| /** | |
| * Sets the sound type(s) that are shown in the picker. | |
| * | |
| * @param type The sound type(s) that are shown in the picker. | |
| * @see RingtoneManager#EXTRA_RINGTONE_TYPE | |
| */ | |
| public void setRingtoneType(int type) { | |
| mRingtoneType = type; | |
| } | |
| /** | |
| * Returns whether to a show an item for the default sound/ringtone. | |
| * | |
| * @return Whether to show an item for the default sound/ringtone. | |
| */ | |
| public boolean getShowDefault() { | |
| return mShowDefault; | |
| } | |
| /** | |
| * Sets whether to show an item for the default sound/ringtone. The default | |
| * to use will be deduced from the sound type(s) being shown. | |
| * | |
| * @param showDefault Whether to show the default or not. | |
| * @see RingtoneManager#EXTRA_RINGTONE_SHOW_DEFAULT | |
| */ | |
| public void setShowDefault(boolean showDefault) { | |
| mShowDefault = showDefault; | |
| } | |
| /** | |
| * Returns whether to a show an item for 'Silent'. | |
| * | |
| * @return Whether to show an item for 'Silent'. | |
| */ | |
| public boolean getShowSilent() { | |
| return mShowSilent; | |
| } | |
| /** | |
| * Sets whether to show an item for 'Silent'. | |
| * | |
| * @param showSilent Whether to show 'Silent'. | |
| * @see RingtoneManager#EXTRA_RINGTONE_SHOW_SILENT | |
| */ | |
| public void setShowSilent(boolean showSilent) { | |
| mShowSilent = showSilent; | |
| } | |
| public int getRequestCode() { | |
| return mRequestCode; | |
| } | |
| /** | |
| * Prepares the intent to launch the ringtone picker. This can be modified | |
| * to adjust the parameters of the ringtone picker. | |
| * | |
| * @param ringtonePickerIntent The ringtone picker intent that can be | |
| * modified by putting extras. | |
| */ | |
| public void onPrepareRingtonePickerIntent(Intent ringtonePickerIntent) { | |
| ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, | |
| onRestoreRingtone()); | |
| ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, mShowDefault); | |
| if (mShowDefault) { | |
| ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, | |
| RingtoneManager.getDefaultUri(getRingtoneType())); | |
| } | |
| ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, mShowSilent); | |
| ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, mRingtoneType); | |
| ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, getTitle()); | |
| ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS, | |
| AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY); | |
| } | |
| /** | |
| * Called when a ringtone is chosen. | |
| * <p> | |
| * By default, this saves the ringtone URI to the persistent storage as a | |
| * string. | |
| * | |
| * @param ringtoneUri The chosen ringtone's {@link Uri}. Can be null. | |
| */ | |
| protected void onSaveRingtone(Uri ringtoneUri) { | |
| persistString(ringtoneUri != null ? ringtoneUri.toString() : ""); | |
| } | |
| /** | |
| * Called when the chooser is about to be shown and the current ringtone | |
| * should be marked. Can return null to not mark any ringtone. | |
| * <p> | |
| * By default, this restores the previous ringtone URI from the persistent | |
| * storage. | |
| * | |
| * @return The ringtone to be marked as the current ringtone. | |
| */ | |
| protected Uri onRestoreRingtone() { | |
| final String uriString = getPersistedString(null); | |
| return !TextUtils.isEmpty(uriString) ? Uri.parse(uriString) : null; | |
| } | |
| @Override | |
| protected Object onGetDefaultValue(TypedArray a, int index) { | |
| return a.getString(index); | |
| } | |
| @Override | |
| protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValueObj) { | |
| String defaultValue = (String) defaultValueObj; | |
| /* | |
| * This method is normally to make sure the internal state and UI | |
| * matches either the persisted value or the default value. Since we | |
| * don't show the current value in the UI (until the dialog is opened) | |
| * and we don't keep local state, if we are restoring the persisted | |
| * value we don't need to do anything. | |
| */ | |
| if (restorePersistedValue) { | |
| return; | |
| } | |
| // If we are setting to the default value, we should persist it. | |
| if (!TextUtils.isEmpty(defaultValue)) { | |
| onSaveRingtone(Uri.parse(defaultValue)); | |
| } | |
| } | |
| protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { | |
| super.onAttachedToHierarchy(preferenceManager); | |
| } | |
| public boolean onActivityResult(int requestCode, int resultCode, Intent data) { | |
| if (data != null) { | |
| Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); | |
| if (callChangeListener(uri != null ? uri.toString() : "")) { | |
| onSaveRingtone(uri); | |
| } | |
| } | |
| return true; | |
| } | |
| public boolean isDefaultRingtone(Uri ringtoneUri) { | |
| // null URIs are valid (None/silence) | |
| return ringtoneUri == null || RingtoneManager.isDefault(ringtoneUri); | |
| } | |
| protected boolean isValidRingtoneUri(Uri ringtoneUri) { | |
| if (isDefaultRingtone(ringtoneUri)) { | |
| return true; | |
| } | |
| // Return early for android resource URIs | |
| if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(ringtoneUri.getScheme())) { | |
| return true; | |
| } | |
| String mimeType = mUserContext.getContentResolver().getType(ringtoneUri); | |
| if (mimeType == null) { | |
| Log.e(TAG, "isValidRingtoneUri for URI:" + ringtoneUri | |
| + " failed: failure to find mimeType (no access from this context?)"); | |
| return false; | |
| } | |
| if (!(mimeType.startsWith("audio/") || mimeType.equals("application/ogg") | |
| || mimeType.equals("application/x-flac"))) { | |
| Log.e(TAG, "isValidRingtoneUri for URI:" + ringtoneUri | |
| + " failed: associated mimeType:" + mimeType + " is not an audio type"); | |
| return false; | |
| } | |
| // Validate userId from URIs: content://{userId}@... | |
| final int userIdFromUri = ContentProvider.getUserIdFromUri(ringtoneUri, mUserId); | |
| if (userIdFromUri != mUserId) { | |
| final UserManager userManager = mUserContext.getSystemService(UserManager.class); | |
| if (!userManager.isSameProfileGroup(mUserId, userIdFromUri)) { | |
| Log.e(TAG, | |
| "isValidRingtoneUri for URI:" + ringtoneUri + " failed: user " + userIdFromUri | |
| + " and user " + mUserId + " are not in the same profile group"); | |
| return false; | |
| } | |
| final int parentUserId; | |
| final int profileUserId; | |
| if (userManager.isProfile()) { | |
| profileUserId = mUserId; | |
| parentUserId = userIdFromUri; | |
| } else { | |
| parentUserId = mUserId; | |
| profileUserId = userIdFromUri; | |
| } | |
| final UserHandle parent = userManager.getProfileParent(UserHandle.of(profileUserId)); | |
| if (parent == null || parent.getIdentifier() != parentUserId) { | |
| Log.e(TAG, | |
| "isValidRingtoneUri for URI:" + ringtoneUri + " failed: user " + profileUserId | |
| + " is not a profile of user " + parentUserId); | |
| return false; | |
| } | |
| // Allow parent <-> managed profile sharing, unless restricted | |
| if (userManager.hasUserRestrictionForUser( | |
| UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE, UserHandle.of(parentUserId))) { | |
| Log.e(TAG, | |
| "isValidRingtoneUri for URI:" + ringtoneUri + " failed: user " + parentUserId | |
| + " has restriction: " + UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE); | |
| return false; | |
| } | |
| if (!(userManager.isManagedProfile(profileUserId) || userManager.getUserProperties( | |
| UserHandle.of(profileUserId)).isMediaSharedWithParent())) { | |
| Log.e(TAG, "isValidRingtoneUri for URI:" + ringtoneUri | |
| + " failed: user " + profileUserId + " is not a cloned or managed profile"); | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| } |