1 /* 2 * Copyright (C) 2022 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 package com.android.quicksearchbox 17 18 import android.content.ContentResolver 19 import android.content.Context 20 import android.content.pm.PackageManager 21 import android.content.pm.PackageManager.NameNotFoundException 22 import android.content.res.Resources 23 import android.graphics.drawable.Drawable 24 import android.net.Uri 25 import android.os.Handler 26 import android.text.TextUtils 27 import android.util.Log 28 import androidx.core.content.ContextCompat 29 import com.android.quicksearchbox.util.* 30 import java.io.FileNotFoundException 31 import java.io.IOException 32 import java.io.InputStream 33 34 /** 35 * Loads icons from other packages. 36 * 37 * Code partly stolen from [ContentResolver] and android.app.SuggestionsAdapter. 38 */ 39 class PackageIconLoader( 40 context: Context?, 41 packageName: String?, 42 uiThread: Handler?, 43 iconLoaderExecutor: NamedTaskExecutor 44 ) : IconLoader { 45 46 private val mContext: Context? 47 48 private val mPackageName: String? 49 50 private var mPackageContext: Context? = null 51 52 private val mUiThread: Handler? 53 54 private val mIconLoaderExecutor: NamedTaskExecutor 55 ensurePackageContextnull56 private fun ensurePackageContext(): Boolean { 57 if (mPackageContext == null) { 58 mPackageContext = 59 try { 60 mContext?.createPackageContext(mPackageName, Context.CONTEXT_RESTRICTED) 61 } catch (ex: PackageManager.NameNotFoundException) { 62 // This should only happen if the app has just be uninstalled 63 Log.e(TAG, "Application not found " + mPackageName) 64 return false 65 } 66 } 67 return true 68 } 69 getIconnull70 override fun getIcon(drawableId: String?): NowOrLater<Drawable?>? { 71 if (DBG) Log.d(TAG, "getIcon($drawableId)") 72 if (TextUtils.isEmpty(drawableId) || "0" == drawableId) { 73 return Now<Drawable>(null) 74 } 75 if (!ensurePackageContext()) { 76 return Now<Drawable>(null) 77 } 78 var drawable: NowOrLater<Drawable?>? 79 try { 80 // First, see if it's just an integer 81 val resourceId: Int = drawableId!!.toInt() 82 // If so, find it by resource ID 83 val icon: Drawable? = ContextCompat.getDrawable(mPackageContext!!, resourceId) 84 drawable = Now(icon) 85 } catch (nfe: NumberFormatException) { 86 // It's not an integer, use it as a URI 87 val uri: Uri = Uri.parse(drawableId) 88 if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())) { 89 // load all resources synchronously, to reduce UI flickering 90 drawable = Now(getDrawable(uri)) 91 } else { 92 drawable = IconLaterTask(uri) 93 } 94 } catch (nfe: Resources.NotFoundException) { 95 // It was an integer, but it couldn't be found, bail out 96 Log.w(TAG, "Icon resource not found: $drawableId") 97 drawable = Now(null) 98 } 99 return drawable 100 } 101 getIconUrinull102 override fun getIconUri(drawableId: String?): Uri? { 103 if (TextUtils.isEmpty(drawableId) || "0" == drawableId) { 104 return null 105 } 106 return if (!ensurePackageContext()) null 107 else 108 try { 109 val resourceId: Int = drawableId!!.toInt() 110 Util.getResourceUri(mPackageContext, resourceId) 111 } catch (nfe: NumberFormatException) { 112 Uri.parse(drawableId) 113 } 114 } 115 116 /** 117 * Gets a drawable by URI. 118 * 119 * @return A drawable, or `null` if the drawable could not be loaded. 120 */ getDrawablenull121 private fun getDrawable(uri: Uri): Drawable? { 122 return try { 123 val scheme: String? = uri.getScheme() 124 if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) { 125 // Load drawables through Resources, to get the source density information 126 val r: OpenResourceIdResult = getResourceId(uri) 127 try { 128 ContextCompat.getDrawable(mPackageContext!!, r.id) 129 } catch (ex: Resources.NotFoundException) { 130 throw FileNotFoundException("Resource does not exist: $uri") 131 } 132 } else { 133 // Let the ContentResolver handle content and file URIs. 134 val stream: InputStream = 135 mPackageContext!!.getContentResolver().openInputStream(uri) 136 ?: throw FileNotFoundException("Failed to open $uri") 137 try { 138 Drawable.createFromStream(stream, null) 139 } finally { 140 try { 141 stream.close() 142 } catch (ex: IOException) { 143 Log.e(TAG, "Error closing icon stream for $uri", ex) 144 } 145 } 146 } 147 } catch (fnfe: FileNotFoundException) { 148 Log.w(TAG, "Icon not found: " + uri + ", " + fnfe.message) 149 null 150 } 151 } 152 153 /** A resource identified by the [Resources] that contains it, and a resource id. */ 154 private inner class OpenResourceIdResult { 155 @JvmField var r: Resources? = null 156 157 @JvmField var id = 0 158 } 159 160 /** Resolves an android.resource URI to a [Resources] and a resource id. */ 161 @Throws(FileNotFoundException::class) getResourceIdnull162 private fun getResourceId(uri: Uri): OpenResourceIdResult { 163 val authority: String? = uri.getAuthority() 164 val r: Resources? = 165 if (TextUtils.isEmpty(authority)) { 166 throw FileNotFoundException("No authority: $uri") 167 } else { 168 try { 169 mPackageContext?.getPackageManager()?.getResourcesForApplication(authority!!) 170 } catch (ex: NameNotFoundException) { 171 throw FileNotFoundException("Failed to get resources: $ex") 172 } 173 } 174 val path: List<String> = uri.getPathSegments() ?: throw FileNotFoundException("No path: $uri") 175 val id: Int = 176 when (path.size) { 177 1 -> { 178 try { 179 Integer.parseInt(path[0]) 180 } catch (e: NumberFormatException) { 181 throw FileNotFoundException("Single path segment is not a resource ID: $uri") 182 } 183 } 184 2 -> { 185 r!!.getIdentifier(path[1], path[0], authority) 186 } 187 else -> { 188 throw FileNotFoundException("More than two path segments: $uri") 189 } 190 } 191 if (id == 0) { 192 throw FileNotFoundException("No resource found for: $uri") 193 } 194 val res = OpenResourceIdResult() 195 res.r = r 196 res.id = id 197 return res 198 } 199 200 private inner class IconLaterTask(iconUri: Uri) : CachedLater<Drawable?>(), NamedTask { 201 private val mUri: Uri 202 203 @Override createnull204 override fun create() { 205 mIconLoaderExecutor.execute(this) 206 } 207 208 @Override runnull209 override fun run() { 210 val icon: Drawable? = icon 211 mUiThread?.post( 212 object : Runnable { 213 override fun run() { 214 store(icon) 215 } 216 } 217 ) 218 } 219 220 @get:Override 221 override val name: String? 222 get() = mPackageName 223 224 // we're making a call into another package, which could throw any exception. 225 // Make sure it doesn't crash QSB 226 private val icon: Drawable? 227 get() = 228 try { 229 getDrawable(mUri) 230 } catch (t: Throwable) { 231 // we're making a call into another package, which could throw any exception. 232 // Make sure it doesn't crash QSB 233 Log.e(TAG, "Failed to load icon $mUri", t) 234 null 235 } 236 237 init { 238 mUri = iconUri 239 } 240 } 241 242 companion object { 243 private const val DBG = false 244 private const val TAG = "QSB.PackageIconLoader" 245 } 246 247 /** 248 * Creates a new icon loader. 249 * 250 * @param context The QSB application context. 251 * @param packageName The name of the package from which the icons will be loaded. 252 * ``` 253 * Resource IDs without an explicit package will be resolved against the package 254 * of this context. 255 * ``` 256 */ 257 init { 258 mContext = context 259 mPackageName = packageName 260 mUiThread = uiThread 261 mIconLoaderExecutor = iconLoaderExecutor 262 } 263 } 264