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