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