1 /*
2  * Copyright (C) 2016 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.deskclock.data;
18 
19 import android.annotation.SuppressLint;
20 import android.content.BroadcastReceiver;
21 import android.content.ContentResolver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.IntentFilter;
25 import android.content.SharedPreferences;
26 import android.content.UriPermission;
27 import android.database.ContentObserver;
28 import android.database.Cursor;
29 import android.media.Ringtone;
30 import android.media.RingtoneManager;
31 import android.net.Uri;
32 import android.os.Handler;
33 import android.provider.Settings;
34 import android.util.ArrayMap;
35 import android.util.ArraySet;
36 
37 import com.android.deskclock.LogUtils;
38 import com.android.deskclock.R;
39 import com.android.deskclock.provider.Alarm;
40 
41 import java.util.Collections;
42 import java.util.List;
43 import java.util.ListIterator;
44 import java.util.Map;
45 import java.util.Set;
46 
47 import static android.media.AudioManager.STREAM_ALARM;
48 import static android.media.RingtoneManager.TITLE_COLUMN_INDEX;
49 
50 /**
51  * All ringtone data is accessed via this model.
52  */
53 final class RingtoneModel {
54 
55     private final Context mContext;
56 
57     private final SharedPreferences mPrefs;
58 
59     /** Maps ringtone uri to ringtone title; looking up a title from scratch is expensive. */
60     private final Map<Uri, String> mRingtoneTitles = new ArrayMap<>(16);
61 
62     /** Clears data structures containing data that is locale-sensitive. */
63     @SuppressWarnings("FieldCanBeLocal")
64     private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
65 
66     /** A mutable copy of the custom ringtones. */
67     private List<CustomRingtone> mCustomRingtones;
68 
RingtoneModel(Context context, SharedPreferences prefs)69     RingtoneModel(Context context, SharedPreferences prefs) {
70         mContext = context;
71         mPrefs = prefs;
72 
73         // Clear caches affected by system settings when system settings change.
74         final ContentResolver cr = mContext.getContentResolver();
75         final ContentObserver observer = new SystemAlarmAlertChangeObserver();
76         cr.registerContentObserver(Settings.System.DEFAULT_ALARM_ALERT_URI, false, observer);
77 
78         // Clear caches affected by locale when locale changes.
79         final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
80         mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
81     }
82 
addCustomRingtone(Uri uri, String title)83     CustomRingtone addCustomRingtone(Uri uri, String title) {
84         // If the uri is already present in an existing ringtone, do nothing.
85         final CustomRingtone existing = getCustomRingtone(uri);
86         if (existing != null) {
87             return existing;
88         }
89 
90         final CustomRingtone ringtone = CustomRingtoneDAO.addCustomRingtone(mPrefs, uri, title);
91         getMutableCustomRingtones().add(ringtone);
92         Collections.sort(getMutableCustomRingtones());
93         return ringtone;
94     }
95 
removeCustomRingtone(Uri uri)96     void removeCustomRingtone(Uri uri) {
97         final List<CustomRingtone> ringtones = getMutableCustomRingtones();
98         for (CustomRingtone ringtone : ringtones) {
99             if (ringtone.getUri().equals(uri)) {
100                 CustomRingtoneDAO.removeCustomRingtone(mPrefs, ringtone.getId());
101                 ringtones.remove(ringtone);
102                 break;
103             }
104         }
105     }
106 
getCustomRingtone(Uri uri)107     private CustomRingtone getCustomRingtone(Uri uri) {
108         for (CustomRingtone ringtone : getMutableCustomRingtones()) {
109             if (ringtone.getUri().equals(uri)) {
110                 return ringtone;
111             }
112         }
113 
114         return null;
115     }
116 
getCustomRingtones()117     List<CustomRingtone> getCustomRingtones() {
118         return Collections.unmodifiableList(getMutableCustomRingtones());
119     }
120 
121     @SuppressLint("NewApi")
loadRingtonePermissions()122     void loadRingtonePermissions() {
123         final List<CustomRingtone> ringtones = getMutableCustomRingtones();
124         if (ringtones.isEmpty()) {
125             return;
126         }
127 
128         final List<UriPermission> uriPermissions =
129                 mContext.getContentResolver().getPersistedUriPermissions();
130         final Set<Uri> permissions = new ArraySet<>(uriPermissions.size());
131         for (UriPermission uriPermission : uriPermissions) {
132             permissions.add(uriPermission.getUri());
133         }
134 
135         for (ListIterator<CustomRingtone> i = ringtones.listIterator(); i.hasNext();) {
136             final CustomRingtone ringtone = i.next();
137             i.set(ringtone.setHasPermissions(permissions.contains(ringtone.getUri())));
138         }
139     }
140 
loadRingtoneTitles()141     void loadRingtoneTitles() {
142         // Early return if the cache is already primed.
143         if (!mRingtoneTitles.isEmpty()) {
144             return;
145         }
146 
147         final RingtoneManager ringtoneManager = new RingtoneManager(mContext);
148         ringtoneManager.setType(STREAM_ALARM);
149 
150         // Cache a title for each system ringtone.
151         try (Cursor cursor = ringtoneManager.getCursor()) {
152             for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
153                 final String ringtoneTitle = cursor.getString(TITLE_COLUMN_INDEX);
154                 final Uri ringtoneUri = ringtoneManager.getRingtoneUri(cursor.getPosition());
155                 mRingtoneTitles.put(ringtoneUri, ringtoneTitle);
156             }
157         } catch (Throwable ignored) {
158             // best attempt only
159             LogUtils.e("Error loading ringtone title cache", ignored);
160         }
161     }
162 
getRingtoneTitle(Uri uri)163     String getRingtoneTitle(Uri uri) {
164         // Special case: no ringtone has a title of "Silent".
165         if (Alarm.NO_RINGTONE_URI.equals(uri)) {
166             return mContext.getString(R.string.silent_ringtone_title);
167         }
168 
169         // If the ringtone is custom, it has its own title.
170         final CustomRingtone customRingtone = getCustomRingtone(uri);
171         if (customRingtone != null) {
172             return customRingtone.getTitle();
173         }
174 
175         // Check the cache.
176         String title = mRingtoneTitles.get(uri);
177 
178         if (title == null) {
179             // This is slow because a media player is created during Ringtone object creation.
180             final Ringtone ringtone = RingtoneManager.getRingtone(mContext, uri);
181             if (ringtone == null) {
182                 LogUtils.e("No ringtone for uri: %s", uri);
183                 return mContext.getString(R.string.unknown_ringtone_title);
184             }
185 
186             // Cache the title for later use.
187             title = ringtone.getTitle(mContext);
188             mRingtoneTitles.put(uri, title);
189         }
190         return title;
191     }
192 
getMutableCustomRingtones()193     private List<CustomRingtone> getMutableCustomRingtones() {
194         if (mCustomRingtones == null) {
195             mCustomRingtones = CustomRingtoneDAO.getCustomRingtones(mPrefs);
196             Collections.sort(mCustomRingtones);
197         }
198 
199         return mCustomRingtones;
200     }
201 
202     /**
203      * This receiver is notified when system settings change. Cached information built on
204      * those system settings must be cleared.
205      */
206     private final class SystemAlarmAlertChangeObserver extends ContentObserver {
207 
SystemAlarmAlertChangeObserver()208         private SystemAlarmAlertChangeObserver() {
209             super(new Handler());
210         }
211 
212         @Override
onChange(boolean selfChange)213         public void onChange(boolean selfChange) {
214             super.onChange(selfChange);
215 
216             // Titles such as "Default ringtone (Oxygen)" are wrong after default ringtone changes.
217             mRingtoneTitles.clear();
218         }
219     }
220 
221     /**
222      * Cached information that is locale-sensitive must be cleared in response to locale changes.
223      */
224     private final class LocaleChangedReceiver extends BroadcastReceiver {
225         @Override
onReceive(Context context, Intent intent)226         public void onReceive(Context context, Intent intent) {
227             // Titles such as "Default ringtone (Oxygen)" are wrong after locale changes.
228             mRingtoneTitles.clear();
229         }
230     }
231 }