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