• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2024 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.customization.picker.themedicon.data.repository
18 
19 import android.content.ContentResolver
20 import android.content.ContentValues
21 import android.content.Context
22 import android.content.Intent
23 import android.content.pm.PackageManager
24 import android.database.ContentObserver
25 import android.net.Uri
26 import com.android.customization.module.CustomizationPreferences
27 import com.android.themepicker.R
28 import com.android.wallpaper.module.InjectorProvider
29 import com.android.wallpaper.picker.di.modules.BackgroundDispatcher
30 import dagger.hilt.android.qualifiers.ApplicationContext
31 import javax.inject.Inject
32 import javax.inject.Singleton
33 import kotlinx.coroutines.CoroutineScope
34 import kotlinx.coroutines.DisposableHandle
35 import kotlinx.coroutines.Job
36 import kotlinx.coroutines.channels.awaitClose
37 import kotlinx.coroutines.flow.Flow
38 import kotlinx.coroutines.flow.MutableStateFlow
39 import kotlinx.coroutines.flow.SharingStarted
40 import kotlinx.coroutines.flow.callbackFlow
41 import kotlinx.coroutines.flow.map
42 import kotlinx.coroutines.flow.stateIn
43 import kotlinx.coroutines.launch
44 
45 @Singleton
46 class ThemedIconRepositoryImpl
47 @Inject
48 constructor(
49     @ApplicationContext private val appContext: Context,
50     private val contentResolver: ContentResolver,
51     packageManager: PackageManager,
52     @BackgroundDispatcher private val backgroundScope: CoroutineScope,
53 ) : ThemedIconRepository {
54     private val uri: MutableStateFlow<Uri?> = MutableStateFlow(null)
55     private var getUriJob: Job =
56         backgroundScope.launch {
57             val homeIntent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
58             val resolveInfo =
59                 packageManager.resolveActivity(
60                     homeIntent,
61                     PackageManager.MATCH_DEFAULT_ONLY or PackageManager.GET_META_DATA,
62                 )
63             val providerAuthority =
64                 resolveInfo
65                     ?.activityInfo
66                     ?.metaData
67                     ?.getString(appContext.getString(R.string.themed_icon_metadata_key))
68             val providerInfo =
69                 providerAuthority?.let { authority ->
70                     val info = packageManager.resolveContentProvider(authority, 0)
71                     val hasPermission =
72                         info?.readPermission?.let {
73                             if (it.isNotEmpty()) {
74                                 appContext.checkSelfPermission(it) ==
75                                     PackageManager.PERMISSION_GRANTED
76                             } else true
77                         } ?: true
78                     if (!hasPermission) {
79                         null
80                     } else {
81                         info
82                     }
83                 }
84             uri.value =
85                 providerInfo?.let {
86                     Uri.Builder()
87                         .scheme(ContentResolver.SCHEME_CONTENT)
88                         .authority(providerInfo.authority)
89                         .appendPath(ICON_THEMED)
90                         .build()
91                 }
92         }
93 
94     override val isAvailable: Flow<Boolean> =
95         uri.map { it != null }
96             .stateIn(
97                 scope = backgroundScope,
98                 started = SharingStarted.WhileSubscribed(),
99                 initialValue = false,
100             )
101 
102     override val isActivated: Flow<Boolean> =
103         callbackFlow {
104                 var disposableHandle: DisposableHandle? = null
105                 launch {
106                     uri.collect {
107                         disposableHandle?.dispose()
108                         if (it != null) {
109                             val contentObserver =
110                                 object : ContentObserver(null) {
111                                     override fun onChange(selfChange: Boolean) {
112                                         trySend(getThemedIconEnabled(it))
113                                     }
114                                 }
115                             contentResolver.registerContentObserver(
116                                 it,
117                                 /* notifyForDescendants= */ true,
118                                 contentObserver,
119                             )
120 
121                             trySend(getThemedIconEnabled(it))
122 
123                             disposableHandle = DisposableHandle {
124                                 contentResolver.unregisterContentObserver(contentObserver)
125                             }
126                         }
127                     }
128                 }
129                 awaitClose { disposableHandle?.dispose() }
130             }
131             .stateIn(
132                 scope = backgroundScope,
133                 started = SharingStarted.WhileSubscribed(),
134                 initialValue = false,
135             )
136 
137     private fun getThemedIconEnabled(uri: Uri): Boolean {
138         val cursor =
139             contentResolver.query(
140                 uri,
141                 /* projection= */ null,
142                 /* selection= */ null,
143                 /* selectionArgs= */ null,
144                 /* sortOrder= */ null,
145             )
146         var isEnabled = false
147         if (cursor != null && cursor.moveToNext()) {
148             isEnabled = (cursor.getInt(cursor.getColumnIndex(COL_ICON_THEMED_VALUE)) == ENABLED)
149             val preferences =
150                 InjectorProvider.getInjector().getPreferences(appContext)
151                     as CustomizationPreferences
152             if (preferences.getThemedIconEnabled() != isEnabled) {
153                 preferences.setThemedIconEnabled(isEnabled)
154             }
155         }
156         cursor?.close()
157         return isEnabled
158     }
159 
160     override suspend fun setThemedIconEnabled(enabled: Boolean) {
161         getUriJob.join()
162         uri.value?.let {
163             val values = ContentValues()
164             values.put(COL_ICON_THEMED_VALUE, enabled)
165             contentResolver.update(it, values, /* where= */ null, /* selectionArgs= */ null)
166         }
167     }
168 
169     companion object {
170         private const val ICON_THEMED = "icon_themed"
171         private const val COL_ICON_THEMED_VALUE = "boolean_value"
172         private const val ENABLED = 1
173     }
174 }
175