• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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