1 /* 2 * Copyright (C) 2007 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings; 18 19 import android.content.ContentProvider; 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.res.TypedArray; 24 import android.media.AudioAttributes; 25 import android.media.RingtoneManager; 26 import android.net.Uri; 27 import android.os.UserHandle; 28 import android.os.UserManager; 29 import android.provider.Settings.System; 30 import android.text.TextUtils; 31 import android.util.AttributeSet; 32 import android.util.Log; 33 34 import androidx.preference.Preference; 35 import androidx.preference.PreferenceManager; 36 37 /** 38 * A {@link Preference} that allows the user to choose a ringtone from those on the device. 39 * The chosen ringtone's URI will be persisted as a string. 40 * <p> 41 * If the user chooses the "Default" item, the saved string will be one of 42 * {@link System#DEFAULT_RINGTONE_URI}, 43 * {@link System#DEFAULT_NOTIFICATION_URI}, or 44 * {@link System#DEFAULT_ALARM_ALERT_URI}. If the user chooses the "Silent" 45 * item, the saved string will be an empty string. 46 * 47 * @attr ref android.R.styleable#RingtonePreference_ringtoneType 48 * @attr ref android.R.styleable#RingtonePreference_showDefault 49 * @attr ref android.R.styleable#RingtonePreference_showSilent 50 * 51 * Based of frameworks/base/core/java/android/preference/RingtonePreference.java 52 * but extends androidx.preference.Preference instead. 53 */ 54 public class RingtonePreference extends Preference { 55 56 private static final String TAG = "RingtonePreference"; 57 58 private int mRingtoneType; 59 private boolean mShowDefault; 60 private boolean mShowSilent; 61 62 private int mRequestCode; 63 protected int mUserId; 64 protected Context mUserContext; 65 RingtonePreference(Context context, AttributeSet attrs)66 public RingtonePreference(Context context, AttributeSet attrs) { 67 super(context, attrs); 68 69 final TypedArray a = context.obtainStyledAttributes(attrs, 70 com.android.internal.R.styleable.RingtonePreference, 0, 0); 71 mRingtoneType = a.getInt(com.android.internal.R.styleable.RingtonePreference_ringtoneType, 72 RingtoneManager.TYPE_RINGTONE); 73 mShowDefault = a.getBoolean(com.android.internal.R.styleable.RingtonePreference_showDefault, 74 true); 75 mShowSilent = a.getBoolean(com.android.internal.R.styleable.RingtonePreference_showSilent, 76 true); 77 setIntent(new Intent(RingtoneManager.ACTION_RINGTONE_PICKER) 78 .setPackage(context.getString(R.string.config_sound_picker_package_name))); 79 setUserId(UserHandle.myUserId()); 80 a.recycle(); 81 } 82 setUserId(int userId)83 public void setUserId(int userId) { 84 mUserId = userId; 85 mUserContext = Utils.createPackageContextAsUser(getContext(), mUserId); 86 } 87 getUserId()88 public int getUserId() { 89 return mUserId; 90 } 91 92 /** 93 * Returns the sound type(s) that are shown in the picker. 94 * 95 * @return The sound type(s) that are shown in the picker. 96 * @see #setRingtoneType(int) 97 */ getRingtoneType()98 public int getRingtoneType() { 99 return mRingtoneType; 100 } 101 102 /** 103 * Sets the sound type(s) that are shown in the picker. 104 * 105 * @param type The sound type(s) that are shown in the picker. 106 * @see RingtoneManager#EXTRA_RINGTONE_TYPE 107 */ setRingtoneType(int type)108 public void setRingtoneType(int type) { 109 mRingtoneType = type; 110 } 111 112 /** 113 * Returns whether to a show an item for the default sound/ringtone. 114 * 115 * @return Whether to show an item for the default sound/ringtone. 116 */ getShowDefault()117 public boolean getShowDefault() { 118 return mShowDefault; 119 } 120 121 /** 122 * Sets whether to show an item for the default sound/ringtone. The default 123 * to use will be deduced from the sound type(s) being shown. 124 * 125 * @param showDefault Whether to show the default or not. 126 * @see RingtoneManager#EXTRA_RINGTONE_SHOW_DEFAULT 127 */ setShowDefault(boolean showDefault)128 public void setShowDefault(boolean showDefault) { 129 mShowDefault = showDefault; 130 } 131 132 /** 133 * Returns whether to a show an item for 'Silent'. 134 * 135 * @return Whether to show an item for 'Silent'. 136 */ getShowSilent()137 public boolean getShowSilent() { 138 return mShowSilent; 139 } 140 141 /** 142 * Sets whether to show an item for 'Silent'. 143 * 144 * @param showSilent Whether to show 'Silent'. 145 * @see RingtoneManager#EXTRA_RINGTONE_SHOW_SILENT 146 */ setShowSilent(boolean showSilent)147 public void setShowSilent(boolean showSilent) { 148 mShowSilent = showSilent; 149 } 150 getRequestCode()151 public int getRequestCode() { 152 return mRequestCode; 153 } 154 155 /** 156 * Prepares the intent to launch the ringtone picker. This can be modified 157 * to adjust the parameters of the ringtone picker. 158 * 159 * @param ringtonePickerIntent The ringtone picker intent that can be 160 * modified by putting extras. 161 */ onPrepareRingtonePickerIntent(Intent ringtonePickerIntent)162 public void onPrepareRingtonePickerIntent(Intent ringtonePickerIntent) { 163 164 ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, 165 onRestoreRingtone()); 166 167 ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, mShowDefault); 168 if (mShowDefault) { 169 ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, 170 RingtoneManager.getDefaultUri(getRingtoneType())); 171 } 172 173 ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, mShowSilent); 174 ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, mRingtoneType); 175 ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, getTitle()); 176 ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_AUDIO_ATTRIBUTES_FLAGS, 177 AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY); 178 } 179 180 /** 181 * Called when a ringtone is chosen. 182 * <p> 183 * By default, this saves the ringtone URI to the persistent storage as a 184 * string. 185 * 186 * @param ringtoneUri The chosen ringtone's {@link Uri}. Can be null. 187 */ onSaveRingtone(Uri ringtoneUri)188 protected void onSaveRingtone(Uri ringtoneUri) { 189 persistString(ringtoneUri != null ? ringtoneUri.toString() : ""); 190 } 191 192 /** 193 * Called when the chooser is about to be shown and the current ringtone 194 * should be marked. Can return null to not mark any ringtone. 195 * <p> 196 * By default, this restores the previous ringtone URI from the persistent 197 * storage. 198 * 199 * @return The ringtone to be marked as the current ringtone. 200 */ onRestoreRingtone()201 protected Uri onRestoreRingtone() { 202 final String uriString = getPersistedString(null); 203 return !TextUtils.isEmpty(uriString) ? Uri.parse(uriString) : null; 204 } 205 206 @Override onGetDefaultValue(TypedArray a, int index)207 protected Object onGetDefaultValue(TypedArray a, int index) { 208 return a.getString(index); 209 } 210 211 @Override onSetInitialValue(boolean restorePersistedValue, Object defaultValueObj)212 protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValueObj) { 213 String defaultValue = (String) defaultValueObj; 214 215 /* 216 * This method is normally to make sure the internal state and UI 217 * matches either the persisted value or the default value. Since we 218 * don't show the current value in the UI (until the dialog is opened) 219 * and we don't keep local state, if we are restoring the persisted 220 * value we don't need to do anything. 221 */ 222 if (restorePersistedValue) { 223 return; 224 } 225 226 // If we are setting to the default value, we should persist it. 227 if (!TextUtils.isEmpty(defaultValue)) { 228 onSaveRingtone(Uri.parse(defaultValue)); 229 } 230 } onAttachedToHierarchy(PreferenceManager preferenceManager)231 protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { 232 super.onAttachedToHierarchy(preferenceManager); 233 } 234 onActivityResult(int requestCode, int resultCode, Intent data)235 public boolean onActivityResult(int requestCode, int resultCode, Intent data) { 236 if (data != null) { 237 Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 238 239 if (callChangeListener(uri != null ? uri.toString() : "")) { 240 onSaveRingtone(uri); 241 } 242 } 243 244 return true; 245 } 246 isDefaultRingtone(Uri ringtoneUri)247 public boolean isDefaultRingtone(Uri ringtoneUri) { 248 // null URIs are valid (None/silence) 249 return ringtoneUri == null || RingtoneManager.isDefault(ringtoneUri); 250 } 251 isValidRingtoneUri(Uri ringtoneUri)252 protected boolean isValidRingtoneUri(Uri ringtoneUri) { 253 if (isDefaultRingtone(ringtoneUri)) { 254 return true; 255 } 256 257 // Return early for android resource URIs 258 if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(ringtoneUri.getScheme())) { 259 return true; 260 } 261 262 String mimeType = mUserContext.getContentResolver().getType(ringtoneUri); 263 if (mimeType == null) { 264 Log.e(TAG, "isValidRingtoneUri for URI:" + ringtoneUri 265 + " failed: failure to find mimeType (no access from this context?)"); 266 return false; 267 } 268 269 if (!(mimeType.startsWith("audio/") || mimeType.equals("application/ogg") 270 || mimeType.equals("application/x-flac"))) { 271 Log.e(TAG, "isValidRingtoneUri for URI:" + ringtoneUri 272 + " failed: associated mimeType:" + mimeType + " is not an audio type"); 273 return false; 274 } 275 276 // Validate userId from URIs: content://{userId}@... 277 final int userIdFromUri = ContentProvider.getUserIdFromUri(ringtoneUri, mUserId); 278 if (userIdFromUri != mUserId) { 279 final UserManager userManager = mUserContext.getSystemService(UserManager.class); 280 281 if (!userManager.isSameProfileGroup(mUserId, userIdFromUri)) { 282 Log.e(TAG, 283 "isValidRingtoneUri for URI:" + ringtoneUri + " failed: user " + userIdFromUri 284 + " and user " + mUserId + " are not in the same profile group"); 285 return false; 286 } 287 288 final int parentUserId; 289 final int profileUserId; 290 if (userManager.isProfile()) { 291 profileUserId = mUserId; 292 parentUserId = userIdFromUri; 293 } else { 294 parentUserId = mUserId; 295 profileUserId = userIdFromUri; 296 } 297 298 final UserHandle parent = userManager.getProfileParent(UserHandle.of(profileUserId)); 299 if (parent == null || parent.getIdentifier() != parentUserId) { 300 Log.e(TAG, 301 "isValidRingtoneUri for URI:" + ringtoneUri + " failed: user " + profileUserId 302 + " is not a profile of user " + parentUserId); 303 return false; 304 } 305 306 // Allow parent <-> managed profile sharing, unless restricted 307 if (userManager.hasUserRestrictionForUser( 308 UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE, UserHandle.of(parentUserId))) { 309 Log.e(TAG, 310 "isValidRingtoneUri for URI:" + ringtoneUri + " failed: user " + parentUserId 311 + " has restriction: " + UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE); 312 return false; 313 } 314 315 if (!(userManager.isManagedProfile(profileUserId) || userManager.getUserProperties( 316 UserHandle.of(profileUserId)).isMediaSharedWithParent())) { 317 Log.e(TAG, "isValidRingtoneUri for URI:" + ringtoneUri 318 + " failed: user " + profileUserId + " is not a cloned or managed profile"); 319 return false; 320 } 321 } 322 323 return true; 324 } 325 326 } 327