Load all icons off main thread

This commit is contained in:
MM20 2021-12-04 14:06:01 +01:00
parent c388b01fe8
commit be50fb2276
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
3 changed files with 150 additions and 92 deletions

View File

@ -81,8 +81,9 @@ class AppShortcut(
override suspend fun loadIcon(context: Context, size: Int): LauncherIcon? { override suspend fun loadIcon(context: Context, size: Int): LauncherIcon? {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
val icon = launcherApps.getShortcutIconDrawable(launcherShortcut, context.resources.displayMetrics.densityDpi) val icon = withContext(Dispatchers.IO) {
icon ?: return null launcherApps.getShortcutIconDrawable(launcherShortcut, context.resources.displayMetrics.densityDpi)
} ?: return null
if (isAtLeastApiLevel(Build.VERSION_CODES.O) && icon is AdaptiveIconDrawable) { if (isAtLeastApiLevel(Build.VERSION_CODES.O) && icon is AdaptiveIconDrawable) {
return LauncherIcon( return LauncherIcon(
foreground = icon.foreground, foreground = icon.foreground,

View File

@ -16,6 +16,8 @@ import de.mm20.launcher2.ktx.asBitmap
import de.mm20.launcher2.ktx.sp import de.mm20.launcher2.ktx.sp
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class Contact( class Contact(
val id: Long, val id: Long,
@ -43,17 +45,23 @@ class Contact(
val iconText = val iconText =
if (firstName.isNotEmpty()) firstName[0].toString() else "" + if (lastName.isNotEmpty()) lastName[0].toString() else "" if (firstName.isNotEmpty()) firstName[0].toString() else "" + if (lastName.isNotEmpty()) lastName[0].toString() else ""
return LauncherIcon( return LauncherIcon(
foreground = TextDrawable(iconText, Color.WHITE, fontSize = 40 * context.sp, typeface = Typeface.DEFAULT_BOLD), foreground = TextDrawable(
iconText,
Color.WHITE,
fontSize = 40 * context.sp,
typeface = Typeface.DEFAULT_BOLD
),
background = ColorDrawable(ContextCompat.getColor(context, R.color.blue)) background = ColorDrawable(ContextCompat.getColor(context, R.color.blue))
) )
} }
override suspend fun loadIcon(context: Context, size: Int): LauncherIcon? { override suspend fun loadIcon(context: Context, size: Int): LauncherIcon? {
val contentResolver = context.contentResolver val contentResolver = context.contentResolver
val uri = ContactsContract.Contacts.getLookupUri(id, lookupKey) ?: return null val bmp = withContext(Dispatchers.IO) {
val bmp = ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri, false) val uri = ContactsContract.Contacts.getLookupUri(id, lookupKey) ?: return@withContext null
?.asBitmap() ContactsContract.Contacts.openContactPhotoInputStream(contentResolver, uri, false)
?: return null ?.asBitmap()
} ?: return null
return LauncherIcon( return LauncherIcon(
foreground = bmp.toDrawable(context.resources) foreground = bmp.toDrawable(context.resources)
) )

View File

@ -1,11 +1,7 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.search.data
import android.Manifest
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteQueryBuilder
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
@ -13,36 +9,33 @@ import android.graphics.drawable.ColorDrawable
import android.location.Geocoder import android.location.Geocoder
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.media.ThumbnailUtils import android.media.ThumbnailUtils
import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.text.format.DateUtils import android.text.format.DateUtils
import android.util.Size import android.util.Size
import androidx.core.content.ContentResolverCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import androidx.exifinterface.media.ExifInterface import androidx.exifinterface.media.ExifInterface
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.checkPermission
import de.mm20.launcher2.ktx.formatToString
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.formatToString
import de.mm20.launcher2.media.ThumbnailUtilsCompat import de.mm20.launcher2.media.ThumbnailUtilsCompat
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherPreferences import de.mm20.launcher2.preferences.LauncherPreferences
import org.json.JSONObject import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
import java.io.File as JavaIOFile import java.io.File as JavaIOFile
open class File( open class File(
val id: Long, val id: Long,
val path: String, val path: String,
val mimeType: String, val mimeType: String,
val size: Long, val size: Long,
val isDirectory: Boolean, val isDirectory: Boolean,
val metaData: List<Pair<Int, String>> val metaData: List<Pair<Int, String>>
) : Searchable() { ) : Searchable() {
override val label = path.substringAfterLast('/') override val label = path.substringAfterLast('/')
@ -55,58 +48,77 @@ open class File(
if (!JavaIOFile(path).exists()) return null if (!JavaIOFile(path).exists()) return null
when { when {
mimeType.startsWith("image/") -> { mimeType.startsWith("image/") -> {
val thumbnail = ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(path), val thumbnail = withContext(Dispatchers.IO) {
size, size) ?: return null ThumbnailUtils.extractThumbnail(
BitmapFactory.decodeFile(path),
size, size
)
} ?: return null
return LauncherIcon( return LauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail), foreground = BitmapDrawable(context.resources, thumbnail),
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
) )
} }
mimeType.startsWith("video/") -> { mimeType.startsWith("video/") -> {
val thumbnail = ThumbnailUtilsCompat.createVideoThumbnail(JavaIOFile(path), val thumbnail = withContext(Dispatchers.IO) {
Size(size, size)) ?: return null ThumbnailUtilsCompat.createVideoThumbnail(
JavaIOFile(path),
Size(size, size)
)
} ?: return null
return LauncherIcon( return LauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail), foreground = BitmapDrawable(context.resources, thumbnail),
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
) )
} }
mimeType.startsWith("audio/") -> { mimeType.startsWith("audio/") -> {
val mediaMetadataRetriever = MediaMetadataRetriever() val thumbnail = withContext(Dispatchers.IO) {
try { val mediaMetadataRetriever = MediaMetadataRetriever()
mediaMetadataRetriever.setDataSource(path) try {
val thumbData = mediaMetadataRetriever.embeddedPicture mediaMetadataRetriever.setDataSource(path)
if (thumbData != null) { val thumbData = mediaMetadataRetriever.embeddedPicture
val thumbnail = ThumbnailUtils.extractThumbnail( if (thumbData != null) {
BitmapFactory.decodeByteArray(thumbData, 0, thumbData.size), size, size) val thumbnail = ThumbnailUtils.extractThumbnail(
mediaMetadataRetriever.release() BitmapFactory.decodeByteArray(thumbData, 0, thumbData.size),
thumbnail ?: return null size,
return LauncherIcon( size
foreground = BitmapDrawable(context.resources, thumbnail), )
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR mediaMetadataRetriever.release()
) return@withContext thumbnail
}
} catch (e: RuntimeException) {
} }
} catch (e: RuntimeException) {
mediaMetadataRetriever.release() mediaMetadataRetriever.release()
return null return@withContext null
} }
thumbnail ?: return null
return LauncherIcon(
foreground = BitmapDrawable(context.resources, thumbnail),
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
)
} }
mimeType == "application/vnd.android.package-archive" -> { mimeType == "application/vnd.android.package-archive" -> {
val pkgInfo = context.packageManager.getPackageArchiveInfo(path, 0) val pkgInfo = context.packageManager.getPackageArchiveInfo(path, 0)
val icon = pkgInfo?.applicationInfo?.loadIcon(context.packageManager) ?: return null val icon = withContext(Dispatchers.IO) {
pkgInfo?.applicationInfo?.loadIcon(context.packageManager)
} ?: return null
when { when {
Build.VERSION.SDK_INT > Build.VERSION_CODES.O && icon is AdaptiveIconDrawable -> { Build.VERSION.SDK_INT > Build.VERSION_CODES.O && icon is AdaptiveIconDrawable -> {
return LauncherIcon( return LauncherIcon(
foreground = icon.foreground, foreground = icon.foreground,
background = icon.background, background = icon.background,
foregroundScale = 1.5f, foregroundScale = 1.5f,
backgroundScale = 1.5f backgroundScale = 1.5f
) )
} }
else -> { else -> {
return LauncherIcon( return LauncherIcon(
foreground = icon, foreground = icon,
foregroundScale = 0.7f, foregroundScale = 0.7f,
autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR autoGenerateBackgroundMode = LauncherIcon.BACKGROUND_COLOR
) )
} }
} }
@ -144,18 +156,20 @@ open class File(
} }
} }
return LauncherIcon( return LauncherIcon(
foreground = context.getDrawable(resId)!!, foreground = context.getDrawable(resId)!!,
background = ColorDrawable(ContextCompat.getColor(context, bgColor)), background = ColorDrawable(ContextCompat.getColor(context, bgColor)),
foregroundScale = 0.5f foregroundScale = 0.5f
) )
} }
override fun getLaunchIntent(context: Context): Intent? { override fun getLaunchIntent(context: Context): Intent? {
val uri = FileProvider.getUriForFile(context, val uri = FileProvider.getUriForFile(
context.applicationContext.packageName + ".fileprovider", JavaIOFile(path)) context,
context.applicationContext.packageName + ".fileprovider", JavaIOFile(path)
)
return Intent(Intent.ACTION_VIEW) return Intent(Intent.ACTION_VIEW)
.setDataAndType(uri, mimeType) .setDataAndType(uri, mimeType)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
fun getFileType(context: Context): String { fun getFileType(context: Context): String {
@ -218,21 +232,28 @@ open class File(
val results = mutableListOf<File>() val results = mutableListOf<File>()
if (!LauncherPreferences.instance.searchFiles) return results if (!LauncherPreferences.instance.searchFiles) return results
if (query.isBlank()) return results if (query.isBlank()) return results
if (!PermissionsManager.checkPermission(context, PermissionsManager.EXTERNAL_STORAGE)) return results if (!PermissionsManager.checkPermission(
val uri = MediaStore.Files.getContentUri("external").buildUpon().appendQueryParameter("limit", "10").build() context,
PermissionsManager.EXTERNAL_STORAGE
)
) return results
val uri = MediaStore.Files.getContentUri("external").buildUpon()
.appendQueryParameter("limit", "10").build()
val projection = arrayOf( val projection = arrayOf(
MediaStore.Files.FileColumns.DISPLAY_NAME, MediaStore.Files.FileColumns.DISPLAY_NAME,
MediaStore.Files.FileColumns._ID, MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.SIZE, MediaStore.Files.FileColumns.SIZE,
MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns.DATA,
MediaStore.Files.FileColumns.MIME_TYPE) MediaStore.Files.FileColumns.MIME_TYPE
val selection = if (query.length > 3) "${MediaStore.Files.FileColumns.TITLE} LIKE ?" else "${MediaStore.Files.FileColumns.TITLE} = ?" )
val selection =
if (query.length > 3) "${MediaStore.Files.FileColumns.TITLE} LIKE ?" else "${MediaStore.Files.FileColumns.TITLE} = ?"
val selArgs = if (query.length > 3) arrayOf("%$query%") else arrayOf(query) val selArgs = if (query.length > 3) arrayOf("%$query%") else arrayOf(query)
val sort = "${MediaStore.Files.FileColumns.DISPLAY_NAME} COLLATE NOCASE ASC" val sort = "${MediaStore.Files.FileColumns.DISPLAY_NAME} COLLATE NOCASE ASC"
val cursor = context.contentResolver.query(uri, projection, selection, selArgs, sort) val cursor = context.contentResolver.query(uri, projection, selection, selArgs, sort)
?: return results ?: return results
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
if (results.size >= 10) { if (results.size >= 10) {
break break
@ -241,14 +262,19 @@ open class File(
if (!JavaIOFile(path).exists()) continue if (!JavaIOFile(path).exists()) continue
val directory = JavaIOFile(path).isDirectory val directory = JavaIOFile(path).isDirectory
val mimeType = (cursor.getStringOrNull(4) val mimeType = (cursor.getStringOrNull(4)
?: if (directory) "inode/directory" else getMimetypeByFileExtension(path.substringAfterLast('.'))) ?: if (directory) "inode/directory" else getMimetypeByFileExtension(
path.substringAfterLast(
'.'
)
))
val file = File( val file = File(
path = path, path = path,
mimeType = mimeType, mimeType = mimeType,
size = cursor.getLong(2), size = cursor.getLong(2),
isDirectory = directory, isDirectory = directory,
id = cursor.getLong(1), id = cursor.getLong(1),
metaData = getMetaData(context, mimeType, path)) metaData = getMetaData(context, mimeType, path)
)
results.add(file) results.add(file)
} }
cursor.close() cursor.close()
@ -281,7 +307,11 @@ open class File(
} }
internal fun getMetaData(context: Context, mimeType: String, path: String): List<Pair<Int, String>> { internal fun getMetaData(
context: Context,
mimeType: String,
path: String
): List<Pair<Int, String>> {
val metaData = mutableListOf<Pair<Int, String>>() val metaData = mutableListOf<Pair<Int, String>>()
when { when {
mimeType.startsWith("audio/") -> { mimeType.startsWith("audio/") -> {
@ -289,14 +319,17 @@ open class File(
try { try {
retriever.setDataSource(path) retriever.setDataSource(path)
arrayOf( arrayOf(
R.string.file_meta_title to MediaMetadataRetriever.METADATA_KEY_TITLE, R.string.file_meta_title to MediaMetadataRetriever.METADATA_KEY_TITLE,
R.string.file_meta_artist to MediaMetadataRetriever.METADATA_KEY_ARTIST, R.string.file_meta_artist to MediaMetadataRetriever.METADATA_KEY_ARTIST,
R.string.file_meta_album to MediaMetadataRetriever.METADATA_KEY_ALBUM, R.string.file_meta_album to MediaMetadataRetriever.METADATA_KEY_ALBUM,
R.string.file_meta_year to MediaMetadataRetriever.METADATA_KEY_YEAR R.string.file_meta_year to MediaMetadataRetriever.METADATA_KEY_YEAR
).forEach { ).forEach {
retriever.extractMetadata(it.second)?.let { m -> metaData.add(it.first to m) } retriever.extractMetadata(it.second)
?.let { m -> metaData.add(it.first to m) }
} }
val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0 val duration =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
?.toLong() ?: 0
val d = DateUtils.formatElapsedTime((duration) / 1000) val d = DateUtils.formatElapsedTime((duration) / 1000)
metaData.add(3, R.string.file_meta_duration to d) metaData.add(3, R.string.file_meta_duration to d)
retriever.release() retriever.release()
@ -308,16 +341,28 @@ open class File(
val retriever = MediaMetadataRetriever() val retriever = MediaMetadataRetriever()
try { try {
retriever.setDataSource(path) retriever.setDataSource(path)
val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toLong() ?: 0 val width =
val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toLong() ?: 0 retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)
?.toLong() ?: 0
val height =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)
?.toLong() ?: 0
metaData.add(R.string.file_meta_dimensions to "${width}x$height") metaData.add(R.string.file_meta_dimensions to "${width}x$height")
val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0 val duration =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
?.toLong() ?: 0
val d = DateUtils.formatElapsedTime(duration / 1000) val d = DateUtils.formatElapsedTime(duration / 1000)
metaData.add(R.string.file_meta_duration to d) metaData.add(R.string.file_meta_duration to d)
val loc = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION) val loc =
retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION)
if (Geocoder.isPresent() && loc != null) { if (Geocoder.isPresent() && loc != null) {
val lon = loc.substring(0, loc.lastIndexOfAny(charArrayOf('+', '-'))).toDouble() val lon =
val lat = loc.substring(loc.lastIndexOfAny(charArrayOf('+', '-')), loc.indexOf('/')).toDouble() loc.substring(0, loc.lastIndexOfAny(charArrayOf('+', '-')))
.toDouble()
val lat = loc.substring(
loc.lastIndexOfAny(charArrayOf('+', '-')),
loc.indexOf('/')
).toDouble()
val list = Geocoder(context).getFromLocation(lon, lat, 1) val list = Geocoder(context).getFromLocation(lon, lat, 1)
if (list.size > 0) { if (list.size > 0) {
metaData.add(R.string.file_meta_location to list[0].formatToString()) metaData.add(R.string.file_meta_location to list[0].formatToString())
@ -350,8 +395,12 @@ open class File(
} }
mimeType == "application/vnd.android.package-archive" -> { mimeType == "application/vnd.android.package-archive" -> {
val pkgInfo = context.packageManager.getPackageArchiveInfo(path, 0) val pkgInfo = context.packageManager.getPackageArchiveInfo(path, 0)
?: return metaData ?: return metaData
metaData.add(R.string.file_meta_app_name to pkgInfo.applicationInfo.loadLabel(context.packageManager).toString()) metaData.add(
R.string.file_meta_app_name to pkgInfo.applicationInfo.loadLabel(
context.packageManager
).toString()
)
metaData.add(R.string.file_meta_app_pkgname to pkgInfo.packageName) metaData.add(R.string.file_meta_app_pkgname to pkgInfo.packageName)
metaData.add(R.string.file_meta_app_version to pkgInfo.versionName) metaData.add(R.string.file_meta_app_version to pkgInfo.versionName)
metaData.add(R.string.file_meta_app_min_sdk to pkgInfo.applicationInfo.minSdkVersion.toString()) metaData.add(R.string.file_meta_app_min_sdk to pkgInfo.applicationInfo.minSdkVersion.toString())