Create appshortcuts module

This commit is contained in:
MM20 2022-03-19 15:46:13 +01:00
parent fb762736a1
commit b73c9fabc9
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
22 changed files with 244 additions and 95 deletions

View File

@ -108,6 +108,7 @@ dependencies {
implementation(project(":accounts")) implementation(project(":accounts"))
implementation(project(":applications")) implementation(project(":applications"))
implementation(project(":appshortcuts"))
implementation(project(":badges")) implementation(project(":badges"))
implementation(project(":base")) implementation(project(":base"))
implementation(project(":calculator")) implementation(project(":calculator"))

View File

@ -6,6 +6,7 @@ import coil.ImageLoaderFactory
import coil.decode.SvgDecoder import coil.decode.SvgDecoder
import de.mm20.launcher2.accounts.accountsModule import de.mm20.launcher2.accounts.accountsModule
import de.mm20.launcher2.applications.applicationsModule import de.mm20.launcher2.applications.applicationsModule
import de.mm20.launcher2.appshortcuts.appShortcutsModule
import de.mm20.launcher2.badges.badgesModule import de.mm20.launcher2.badges.badgesModule
import de.mm20.launcher2.calculator.calculatorModule import de.mm20.launcher2.calculator.calculatorModule
import de.mm20.launcher2.calendar.calendarModule import de.mm20.launcher2.calendar.calendarModule
@ -50,6 +51,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
listOf( listOf(
accountsModule, accountsModule,
applicationsModule, applicationsModule,
appShortcutsModule,
calculatorModule, calculatorModule,
badgesModule, badgesModule,
calendarModule, calendarModule,

View File

@ -46,65 +46,3 @@ class LauncherAppDeserializer(val context: Context) : SearchableDeserializer {
} }
} }
class AppShortcutSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
searchable as AppShortcut
return jsonObjectOf(
"packagename" to searchable.launcherShortcut.`package`,
"id" to searchable.launcherShortcut.id,
"user" to searchable.userSerialNumber,
).toString()
}
override val typePrefix: String
get() = "shortcut"
}
class AppShortcutDeserializer(
val context: Context
) : SearchableDeserializer, KoinComponent {
override fun deserialize(serialized: String): Searchable? {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
if (!launcherApps.hasShortcutHostPermission()) return null
else {
val json = JSONObject(serialized)
val packageName = json.getString("packagename")
val id = json.getString("id")
val userSerial = json.optLong("user")
val query = LauncherApps.ShortcutQuery()
query.setPackage(packageName)
query.setQueryFlags(
LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or
LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST or
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED
)
query.setShortcutIds(mutableListOf(id))
val userManager = context.getSystemService<UserManager>()!!
val user = userManager.getUserForSerialNumber(userSerial) ?: Process.myUserHandle()
val shortcuts = try {
launcherApps.getShortcuts(query, user)
} catch (e: IllegalStateException) {
return null
}
val pm = context.packageManager
val appName = try {
pm.getApplicationInfo(packageName, 0).loadLabel(pm).toString()
} catch (e: PackageManager.NameNotFoundException) {
return null
}
if (shortcuts == null || shortcuts.isEmpty()) {
return null
} else {
val activity = shortcuts[0].activity
return AppShortcut(
context = context,
launcherShortcut = shortcuts[0],
appName = appName
)
}
}
}
}

View File

@ -17,7 +17,6 @@ abstract class Application(
val activity: String, val activity: String,
val flags: Int, val flags: Int,
val version: String?, val version: String?,
val shortcuts: List<AppShortcut> = emptyList()
) : Searchable() { ) : Searchable() {
override fun serialize(): String { override fun serialize(): String {

View File

@ -25,35 +25,13 @@ import org.koin.core.component.KoinComponent
*/ */
class LauncherApp( class LauncherApp(
context: Context, context: Context,
public val launcherActivityInfo: LauncherActivityInfo val launcherActivityInfo: LauncherActivityInfo
) : Application( ) : Application(
label = launcherActivityInfo.label.toString(), label = launcherActivityInfo.label.toString(),
`package` = launcherActivityInfo.applicationInfo.packageName, `package` = launcherActivityInfo.applicationInfo.packageName,
activity = launcherActivityInfo.name, activity = launcherActivityInfo.name,
flags = launcherActivityInfo.applicationInfo.flags, flags = launcherActivityInfo.applicationInfo.flags,
version = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName), version = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName),
shortcuts = run {
val appShortcuts = mutableListOf<AppShortcut>()
val launcherApps = context.getSystemService<LauncherApps>()!!
if (!launcherApps.hasShortcutHostPermission()) return@run appShortcuts
val query = LauncherApps.ShortcutQuery()
.setPackage(launcherActivityInfo.applicationInfo.packageName)
.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST)
val shortcuts = try {
launcherApps.getShortcuts(query, launcherActivityInfo.user)
} catch (e: IllegalStateException) {
emptyList<ShortcutInfo>()
}
appShortcuts.addAll(shortcuts?.map {
AppShortcut(
context,
it,
launcherActivityInfo.label.toString()
)
}
?: emptyList())
appShortcuts
}
), KoinComponent { ), KoinComponent {
internal val userSerialNumber: Long = launcherActivityInfo.user.getSerialNumber(context) internal val userSerialNumber: Long = launcherActivityInfo.user.getSerialNumber(context)

1
appshortcuts/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,51 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.koin.android)
implementation(libs.commons.text)
implementation(libs.tinypinyin)
implementation(project(":search"))
implementation(project(":base"))
implementation(project(":preferences"))
implementation(project(":ktx"))
}

View File

21
appshortcuts/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.mm20.launcher2.appshortcuts">
</manifest>

View File

@ -0,0 +1,51 @@
package de.mm20.launcher2.appshortcuts
import android.content.Context
import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo
import android.os.UserHandle
import androidx.core.content.getSystemService
import de.mm20.launcher2.search.data.AppShortcut
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
interface AppShortcutRepository {
suspend fun getShortcutsForActivity(launcherActivityInfo: LauncherActivityInfo, count: Int = 5): List<AppShortcut>
}
internal class AppShortcutRepositoryImpl(
private val context: Context
): AppShortcutRepository {
override suspend fun getShortcutsForActivity(
launcherActivityInfo: LauncherActivityInfo,
count: Int,
) = withContext(Dispatchers.IO){
val launcherApps = context.getSystemService<LauncherApps>()!!
if (!launcherApps.hasShortcutHostPermission()) return@withContext emptyList()
val query = LauncherApps.ShortcutQuery()
.setPackage(launcherActivityInfo.applicationInfo.packageName)
.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST)
val shortcuts = try {
launcherApps.getShortcuts(query, launcherActivityInfo.user)
} catch (e: IllegalStateException) {
emptyList()
}
val appShortcuts = mutableListOf<AppShortcut>()
appShortcuts.addAll(shortcuts
?.let {
if (it.size > count) it.subList(0, count)
else it
}
?.map {
AppShortcut(
context,
it,
launcherActivityInfo.label.toString()
)
} ?: emptyList())
appShortcuts
}
}

View File

@ -0,0 +1,78 @@
package de.mm20.launcher2.appshortcuts
import android.content.Context
import android.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.os.Process
import android.os.UserManager
import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.Searchable
import org.json.JSONObject
import org.koin.core.component.KoinComponent
class AppShortcutSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
searchable as AppShortcut
return jsonObjectOf(
"packagename" to searchable.launcherShortcut.`package`,
"id" to searchable.launcherShortcut.id,
"user" to searchable.userSerialNumber,
).toString()
}
override val typePrefix: String
get() = "shortcut"
}
class AppShortcutDeserializer(
val context: Context
) : SearchableDeserializer, KoinComponent {
override fun deserialize(serialized: String): Searchable? {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
if (!launcherApps.hasShortcutHostPermission()) return null
else {
val json = JSONObject(serialized)
val packageName = json.getString("packagename")
val id = json.getString("id")
val userSerial = json.optLong("user")
val query = LauncherApps.ShortcutQuery()
query.setPackage(packageName)
query.setQueryFlags(
LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or
LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST or
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED
)
query.setShortcutIds(mutableListOf(id))
val userManager = context.getSystemService<UserManager>()!!
val user = userManager.getUserForSerialNumber(userSerial) ?: Process.myUserHandle()
val shortcuts = try {
launcherApps.getShortcuts(query, user)
} catch (e: IllegalStateException) {
return null
}
val pm = context.packageManager
val appName = try {
pm.getApplicationInfo(packageName, 0).loadLabel(pm).toString()
} catch (e: PackageManager.NameNotFoundException) {
return null
}
if (shortcuts == null || shortcuts.isEmpty()) {
return null
} else {
val activity = shortcuts[0].activity
return AppShortcut(
context = context,
launcherShortcut = shortcuts[0],
appName = appName
)
}
}
}
}

View File

@ -0,0 +1,8 @@
package de.mm20.launcher2.appshortcuts
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val appShortcutsModule = module {
single<AppShortcutRepository> { AppShortcutRepositoryImpl(androidContext()) }
}

View File

@ -6,17 +6,13 @@ import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo import android.content.pm.ShortcutInfo
import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.AdaptiveIconDrawable
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Process import android.os.Process
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import de.mm20.launcher2.applications.R import de.mm20.launcher2.appshortcuts.R
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.getSerialNumber import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -63,7 +59,11 @@ class AppShortcut(
) )
} }
override suspend fun loadIcon(context: Context, size: Int, legacyIconBackground: LegacyIconBackground): LauncherIcon? { override suspend fun loadIcon(
context: Context,
size: Int,
legacyIconBackground: LegacyIconBackground
): LauncherIcon? {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
val icon = withContext(Dispatchers.IO) { val icon = withContext(Dispatchers.IO) {
launcherApps.getShortcutIconDrawable( launcherApps.getShortcutIconDrawable(

View File

@ -44,6 +44,7 @@ dependencies {
implementation(project(":ktx")) implementation(project(":ktx"))
implementation(project(":applications")) implementation(project(":applications"))
implementation(project(":appshortcuts"))
implementation(project(":notifications")) implementation(project(":notifications"))
implementation(project(":preferences")) implementation(project(":preferences"))
implementation(project(":base")) implementation(project(":base"))

View File

@ -1,6 +1,5 @@
package de.mm20.launcher2.badges.providers package de.mm20.launcher2.badges.providers
import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge

View File

@ -46,6 +46,7 @@ dependencies {
implementation(project(":database")) implementation(project(":database"))
implementation(project(":preferences")) implementation(project(":preferences"))
implementation(project(":applications")) implementation(project(":applications"))
implementation(project(":appshortcuts"))
implementation(project(":contacts")) implementation(project(":contacts"))
implementation(project(":ktx")) implementation(project(":ktx"))
implementation(project(":files")) implementation(project(":files"))

View File

@ -1,5 +1,7 @@
package de.mm20.launcher2.favorites package de.mm20.launcher2.favorites
import de.mm20.launcher2.appshortcuts.AppShortcutDeserializer
import de.mm20.launcher2.appshortcuts.AppShortcutSerializer
import de.mm20.launcher2.calendar.CalendarEventDeserializer import de.mm20.launcher2.calendar.CalendarEventDeserializer
import de.mm20.launcher2.calendar.CalendarEventSerializer import de.mm20.launcher2.calendar.CalendarEventSerializer
import de.mm20.launcher2.contacts.ContactDeserializer import de.mm20.launcher2.contacts.ContactDeserializer
@ -13,7 +15,6 @@ import de.mm20.launcher2.websites.WebsiteSerializer
import de.mm20.launcher2.wikipedia.WikipediaDeserializer import de.mm20.launcher2.wikipedia.WikipediaDeserializer
import de.mm20.launcher2.wikipedia.WikipediaSerializer import de.mm20.launcher2.wikipedia.WikipediaSerializer
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
val favoritesModule = module { val favoritesModule = module {

View File

@ -384,3 +384,4 @@ dependencyResolutionManagement {
} }
include(":notifications") include(":notifications")
include(":accounts") include(":accounts")
include(":appshortcuts")

View File

@ -115,6 +115,7 @@ dependencies {
implementation(project(":search")) implementation(project(":search"))
implementation(project(":preferences")) implementation(project(":preferences"))
implementation(project(":applications")) implementation(project(":applications"))
implementation(project(":appshortcuts"))
implementation(project(":calculator")) implementation(project(":calculator"))
implementation(project(":files")) implementation(project(":files"))
implementation(project(":widgets")) implementation(project(":widgets"))

View File

@ -112,7 +112,9 @@ fun AppItem(
) )
} }
for (shortcut in app.shortcuts.subList(0, min(app.shortcuts.size, 5))) { val shortcuts by viewModel.shortcuts.collectAsState(emptyList())
for (shortcut in shortcuts) {
val title = val title =
shortcut.launcherShortcut.shortLabel shortcut.launcherShortcut.shortLabel
?: shortcut.launcherShortcut.longLabel ?: shortcut.launcherShortcut.longLabel

View File

@ -10,14 +10,17 @@ import android.provider.Settings
import android.service.notification.StatusBarNotification import android.service.notification.StatusBarNotification
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.notifications.NotificationRepository import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.Application import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.inject import org.koin.core.component.inject
@ -26,6 +29,7 @@ class AppItemVM(
private val app: Application private val app: Application
) : SearchableItemVM(app) { ) : SearchableItemVM(app) {
private val notificationRepository: NotificationRepository by inject() private val notificationRepository: NotificationRepository by inject()
private val appShortcutRepository: AppShortcutRepository by inject()
val notifications = val notifications =
@ -105,6 +109,12 @@ class AppItemVM(
return launcherApps.getShortcutIconDrawable(shortcut, 0) return launcherApps.getShortcutIconDrawable(shortcut, 0)
} }
val shortcuts = flow {
if (app is LauncherApp) {
emit(appShortcutRepository.getShortcutsForActivity(app.launcherActivityInfo, 5))
}
}
fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> { fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> {
return favoritesRepository.isPinned(shortcut) return favoritesRepository.isPinned(shortcut)
} }