| /* |
| * Copyright (C) 2016 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.deskclock.data; |
| |
| import static android.media.AudioManager.STREAM_ALARM; |
| import static android.media.RingtoneManager.TITLE_COLUMN_INDEX; |
| |
| import android.annotation.SuppressLint; |
| import android.content.BroadcastReceiver; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.SharedPreferences; |
| import android.content.UriPermission; |
| import android.database.ContentObserver; |
| import android.database.Cursor; |
| import android.media.Ringtone; |
| import android.media.RingtoneManager; |
| import android.net.Uri; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.provider.Settings; |
| import android.util.ArrayMap; |
| import android.util.ArraySet; |
| |
| import com.android.deskclock.LogUtils; |
| import com.android.deskclock.R; |
| import com.android.deskclock.provider.Alarm; |
| |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.ListIterator; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * All ringtone data is accessed via this model. |
| */ |
| final class RingtoneModel { |
| |
| private final Context mContext; |
| |
| private final SharedPreferences mPrefs; |
| |
| /** Maps ringtone uri to ringtone title; looking up a title from scratch is expensive. */ |
| private final Map<Uri, String> mRingtoneTitles = new ArrayMap<>(16); |
| |
| /** Clears data structures containing data that is locale-sensitive. */ |
| @SuppressWarnings("FieldCanBeLocal") |
| private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver(); |
| |
| /** A mutable copy of the custom ringtones. */ |
| private List<CustomRingtone> mCustomRingtones; |
| |
| RingtoneModel(Context context, SharedPreferences prefs) { |
| mContext = context; |
| mPrefs = prefs; |
| |
| // Clear caches affected by system settings when system settings change. |
| final ContentResolver cr = mContext.getContentResolver(); |
| final ContentObserver observer = new SystemAlarmAlertChangeObserver(); |
| cr.registerContentObserver(Settings.System.DEFAULT_ALARM_ALERT_URI, false, observer); |
| |
| // Clear caches affected by locale when locale changes. |
| final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED); |
| mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter); |
| } |
| |
| void addCustomRingtone(Uri uri, String title) { |
| // If the uri is already present in an existing ringtone, do nothing. |
| final CustomRingtone existing = getCustomRingtone(uri); |
| if (existing != null) { |
| return; |
| } |
| |
| final CustomRingtone ringtone = CustomRingtoneDAO.addCustomRingtone(mPrefs, uri, title); |
| getMutableCustomRingtones().add(ringtone); |
| Collections.sort(getMutableCustomRingtones()); |
| } |
| |
| void removeCustomRingtone(Uri uri) { |
| final List<CustomRingtone> ringtones = getMutableCustomRingtones(); |
| for (CustomRingtone ringtone : ringtones) { |
| if (ringtone.getUri().equals(uri)) { |
| CustomRingtoneDAO.removeCustomRingtone(mPrefs, ringtone.getId()); |
| ringtones.remove(ringtone); |
| break; |
| } |
| } |
| } |
| |
| private CustomRingtone getCustomRingtone(Uri uri) { |
| for (CustomRingtone ringtone : getMutableCustomRingtones()) { |
| if (ringtone.getUri().equals(uri)) { |
| return ringtone; |
| } |
| } |
| |
| return null; |
| } |
| |
| List<CustomRingtone> getCustomRingtones() { |
| return Collections.unmodifiableList(getMutableCustomRingtones()); |
| } |
| |
| @SuppressLint("NewApi") |
| void loadRingtonePermissions() { |
| final List<CustomRingtone> ringtones = getMutableCustomRingtones(); |
| if (ringtones.isEmpty()) { |
| return; |
| } |
| |
| final List<UriPermission> uriPermissions = |
| mContext.getContentResolver().getPersistedUriPermissions(); |
| final Set<Uri> permissions = new ArraySet<>(uriPermissions.size()); |
| for (UriPermission uriPermission : uriPermissions) { |
| permissions.add(uriPermission.getUri()); |
| } |
| |
| for (ListIterator<CustomRingtone> i = ringtones.listIterator(); i.hasNext();) { |
| final CustomRingtone ringtone = i.next(); |
| i.set(ringtone.setHasPermissions(permissions.contains(ringtone.getUri()))); |
| } |
| } |
| |
| void loadRingtoneTitles() { |
| // Early return if the cache is already primed. |
| if (!mRingtoneTitles.isEmpty()) { |
| return; |
| } |
| |
| final RingtoneManager ringtoneManager = new RingtoneManager(mContext); |
| ringtoneManager.setType(STREAM_ALARM); |
| |
| // Cache a title for each system ringtone. |
| try (Cursor cursor = ringtoneManager.getCursor()) { |
| for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) { |
| final String ringtoneTitle = cursor.getString(TITLE_COLUMN_INDEX); |
| final Uri ringtoneUri = ringtoneManager.getRingtoneUri(cursor.getPosition()); |
| mRingtoneTitles.put(ringtoneUri, ringtoneTitle); |
| } |
| } catch (Throwable t) { |
| // best attempt only |
| LogUtils.e("Error loading ringtone title cache", t); |
| } |
| } |
| |
| String getRingtoneTitle(Uri uri) { |
| // Special case: no ringtone has a title of "Silent". |
| if (Alarm.NO_RINGTONE_URI.equals(uri)) { |
| return mContext.getString(R.string.silent_ringtone_title); |
| } |
| |
| // If the ringtone is custom, it has its own title. |
| final CustomRingtone customRingtone = getCustomRingtone(uri); |
| if (customRingtone != null) { |
| return customRingtone.getTitle(); |
| } |
| |
| // Check the cache. |
| String title = mRingtoneTitles.get(uri); |
| |
| if (title == null) { |
| // This is slow because a media player is created during Ringtone object creation. |
| final Ringtone ringtone = RingtoneManager.getRingtone(mContext, uri); |
| if (ringtone == null) { |
| LogUtils.e("No ringtone for uri: %s", uri); |
| return mContext.getString(R.string.unknown_ringtone_title); |
| } |
| |
| // Cache the title for later use. |
| title = ringtone.getTitle(mContext); |
| mRingtoneTitles.put(uri, title); |
| } |
| return title; |
| } |
| |
| private List<CustomRingtone> getMutableCustomRingtones() { |
| if (mCustomRingtones == null) { |
| mCustomRingtones = CustomRingtoneDAO.getCustomRingtones(mPrefs); |
| Collections.sort(mCustomRingtones); |
| } |
| |
| return mCustomRingtones; |
| } |
| |
| /** |
| * This receiver is notified when system settings change. Cached information built on |
| * those system settings must be cleared. |
| */ |
| private final class SystemAlarmAlertChangeObserver extends ContentObserver { |
| |
| private SystemAlarmAlertChangeObserver() { |
| super(new Handler(Looper.myLooper())); |
| } |
| |
| @Override |
| public void onChange(boolean selfChange) { |
| super.onChange(selfChange); |
| |
| // Titles such as "Default ringtone (Oxygen)" are wrong after default ringtone changes. |
| mRingtoneTitles.clear(); |
| } |
| } |
| |
| /** |
| * Cached information that is locale-sensitive must be cleared in response to locale changes. |
| */ |
| private final class LocaleChangedReceiver extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| // Titles such as "Default ringtone (Oxygen)" are wrong after locale changes. |
| mRingtoneTitles.clear(); |
| } |
| } |
| } |