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 }