/* * 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. *

* 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)); 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. *

* 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. *

* 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; } }