blob: 530d1b10c0a9ff63e04d0e37ca0ec4e1857f1084 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quicksearchbox
import android.content.ContentResolver
import android.content.Context
import android.content.pm.PackageManager
import android.content.pm.PackageManager.NameNotFoundException
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Handler
import android.text.TextUtils
import android.util.Log
import androidx.core.content.ContextCompat
import com.android.quicksearchbox.util.*
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
/**
* Loads icons from other packages.
*
* Code partly stolen from [ContentResolver] and android.app.SuggestionsAdapter.
*/
class PackageIconLoader(
context: Context?,
packageName: String?,
uiThread: Handler?,
iconLoaderExecutor: NamedTaskExecutor
) : IconLoader {
private val mContext: Context?
private val mPackageName: String?
private var mPackageContext: Context? = null
private val mUiThread: Handler?
private val mIconLoaderExecutor: NamedTaskExecutor
private fun ensurePackageContext(): Boolean {
if (mPackageContext == null) {
mPackageContext =
try {
mContext?.createPackageContext(mPackageName, Context.CONTEXT_RESTRICTED)
} catch (ex: PackageManager.NameNotFoundException) {
// This should only happen if the app has just be uninstalled
Log.e(TAG, "Application not found " + mPackageName)
return false
}
}
return true
}
override fun getIcon(drawableId: String?): NowOrLater<Drawable?>? {
if (DBG) Log.d(TAG, "getIcon($drawableId)")
if (TextUtils.isEmpty(drawableId) || "0" == drawableId) {
return Now<Drawable>(null)
}
if (!ensurePackageContext()) {
return Now<Drawable>(null)
}
var drawable: NowOrLater<Drawable?>?
try {
// First, see if it's just an integer
val resourceId: Int = drawableId!!.toInt()
// If so, find it by resource ID
val icon: Drawable? = ContextCompat.getDrawable(mPackageContext!!, resourceId)
drawable = Now(icon)
} catch (nfe: NumberFormatException) {
// It's not an integer, use it as a URI
val uri: Uri = Uri.parse(drawableId)
if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())) {
// load all resources synchronously, to reduce UI flickering
drawable = Now(getDrawable(uri))
} else {
drawable = IconLaterTask(uri)
}
} catch (nfe: Resources.NotFoundException) {
// It was an integer, but it couldn't be found, bail out
Log.w(TAG, "Icon resource not found: $drawableId")
drawable = Now(null)
}
return drawable
}
override fun getIconUri(drawableId: String?): Uri? {
if (TextUtils.isEmpty(drawableId) || "0" == drawableId) {
return null
}
return if (!ensurePackageContext()) null
else
try {
val resourceId: Int = drawableId!!.toInt()
Util.getResourceUri(mPackageContext, resourceId)
} catch (nfe: NumberFormatException) {
Uri.parse(drawableId)
}
}
/**
* Gets a drawable by URI.
*
* @return A drawable, or `null` if the drawable could not be loaded.
*/
private fun getDrawable(uri: Uri): Drawable? {
return try {
val scheme: String? = uri.getScheme()
if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
// Load drawables through Resources, to get the source density information
val r: OpenResourceIdResult = getResourceId(uri)
try {
ContextCompat.getDrawable(mPackageContext!!, r.id)
} catch (ex: Resources.NotFoundException) {
throw FileNotFoundException("Resource does not exist: $uri")
}
} else {
// Let the ContentResolver handle content and file URIs.
val stream: InputStream =
mPackageContext!!.getContentResolver().openInputStream(uri)
?: throw FileNotFoundException("Failed to open $uri")
try {
Drawable.createFromStream(stream, null)
} finally {
try {
stream.close()
} catch (ex: IOException) {
Log.e(TAG, "Error closing icon stream for $uri", ex)
}
}
}
} catch (fnfe: FileNotFoundException) {
Log.w(TAG, "Icon not found: " + uri + ", " + fnfe.message)
null
}
}
/** A resource identified by the [Resources] that contains it, and a resource id. */
private inner class OpenResourceIdResult {
@JvmField var r: Resources? = null
@JvmField var id = 0
}
/** Resolves an android.resource URI to a [Resources] and a resource id. */
@Throws(FileNotFoundException::class)
private fun getResourceId(uri: Uri): OpenResourceIdResult {
val authority: String? = uri.getAuthority()
val r: Resources? =
if (TextUtils.isEmpty(authority)) {
throw FileNotFoundException("No authority: $uri")
} else {
try {
mPackageContext?.getPackageManager()?.getResourcesForApplication(authority!!)
} catch (ex: NameNotFoundException) {
throw FileNotFoundException("Failed to get resources: $ex")
}
}
val path: List<String> = uri.getPathSegments() ?: throw FileNotFoundException("No path: $uri")
val id: Int =
when (path.size) {
1 -> {
try {
Integer.parseInt(path[0])
} catch (e: NumberFormatException) {
throw FileNotFoundException("Single path segment is not a resource ID: $uri")
}
}
2 -> {
r!!.getIdentifier(path[1], path[0], authority)
}
else -> {
throw FileNotFoundException("More than two path segments: $uri")
}
}
if (id == 0) {
throw FileNotFoundException("No resource found for: $uri")
}
val res = OpenResourceIdResult()
res.r = r
res.id = id
return res
}
private inner class IconLaterTask(iconUri: Uri) : CachedLater<Drawable?>(), NamedTask {
private val mUri: Uri
@Override
override fun create() {
mIconLoaderExecutor.execute(this)
}
@Override
override fun run() {
val icon: Drawable? = icon
mUiThread?.post(
object : Runnable {
override fun run() {
store(icon)
}
}
)
}
@get:Override
override val name: String?
get() = mPackageName
// we're making a call into another package, which could throw any exception.
// Make sure it doesn't crash QSB
private val icon: Drawable?
get() =
try {
getDrawable(mUri)
} catch (t: Throwable) {
// we're making a call into another package, which could throw any exception.
// Make sure it doesn't crash QSB
Log.e(TAG, "Failed to load icon $mUri", t)
null
}
init {
mUri = iconUri
}
}
companion object {
private const val DBG = false
private const val TAG = "QSB.PackageIconLoader"
}
/**
* Creates a new icon loader.
*
* @param context The QSB application context.
* @param packageName The name of the package from which the icons will be loaded.
* ```
* Resource IDs without an explicit package will be resolved against the package
* of this context.
* ```
*/
init {
mContext = context
mPackageName = packageName
mUiThread = uiThread
mIconLoaderExecutor = iconLoaderExecutor
}
}