diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 81ab4205..47f39c57 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -108,6 +108,7 @@ dependencies { implementation(project(":accounts")) implementation(project(":applications")) + implementation(project(":appshortcuts")) implementation(project(":badges")) implementation(project(":base")) implementation(project(":calculator")) diff --git a/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt b/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt index f6aa1e8d..d79f6db0 100644 --- a/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt +++ b/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt @@ -6,6 +6,7 @@ import coil.ImageLoaderFactory import coil.decode.SvgDecoder import de.mm20.launcher2.accounts.accountsModule import de.mm20.launcher2.applications.applicationsModule +import de.mm20.launcher2.appshortcuts.appShortcutsModule import de.mm20.launcher2.badges.badgesModule import de.mm20.launcher2.calculator.calculatorModule import de.mm20.launcher2.calendar.calendarModule @@ -50,6 +51,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory { listOf( accountsModule, applicationsModule, + appShortcutsModule, calculatorModule, badgesModule, calendarModule, diff --git a/applications/src/main/java/de/mm20/launcher2/search/data/AppSerialization.kt b/applications/src/main/java/de/mm20/launcher2/search/data/AppSerialization.kt index 26e61abf..b88cee27 100644 --- a/applications/src/main/java/de/mm20/launcher2/search/data/AppSerialization.kt +++ b/applications/src/main/java/de/mm20/launcher2/search/data/AppSerialization.kt @@ -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()!! - 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 - ) - } - } - } -} \ No newline at end of file diff --git a/applications/src/main/java/de/mm20/launcher2/search/data/Application.kt b/applications/src/main/java/de/mm20/launcher2/search/data/Application.kt index 663c5f4f..dd35e6d2 100644 --- a/applications/src/main/java/de/mm20/launcher2/search/data/Application.kt +++ b/applications/src/main/java/de/mm20/launcher2/search/data/Application.kt @@ -17,7 +17,6 @@ abstract class Application( val activity: String, val flags: Int, val version: String?, - val shortcuts: List = emptyList() ) : Searchable() { override fun serialize(): String { diff --git a/applications/src/main/java/de/mm20/launcher2/search/data/LauncherApp.kt b/applications/src/main/java/de/mm20/launcher2/search/data/LauncherApp.kt index 1e957953..6672e7cd 100644 --- a/applications/src/main/java/de/mm20/launcher2/search/data/LauncherApp.kt +++ b/applications/src/main/java/de/mm20/launcher2/search/data/LauncherApp.kt @@ -25,35 +25,13 @@ import org.koin.core.component.KoinComponent */ class LauncherApp( context: Context, - public val launcherActivityInfo: LauncherActivityInfo + val launcherActivityInfo: LauncherActivityInfo ) : Application( label = launcherActivityInfo.label.toString(), `package` = launcherActivityInfo.applicationInfo.packageName, activity = launcherActivityInfo.name, flags = launcherActivityInfo.applicationInfo.flags, version = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName), - shortcuts = run { - val appShortcuts = mutableListOf() - val launcherApps = context.getSystemService()!! - 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() - } - appShortcuts.addAll(shortcuts?.map { - AppShortcut( - context, - it, - launcherActivityInfo.label.toString() - ) - } - ?: emptyList()) - appShortcuts - } ), KoinComponent { internal val userSerialNumber: Long = launcherActivityInfo.user.getSerialNumber(context) diff --git a/appshortcuts/.gitignore b/appshortcuts/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/appshortcuts/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/appshortcuts/build.gradle.kts b/appshortcuts/build.gradle.kts new file mode 100644 index 00000000..8dee88cf --- /dev/null +++ b/appshortcuts/build.gradle.kts @@ -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")) + +} \ No newline at end of file diff --git a/appshortcuts/consumer-rules.pro b/appshortcuts/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/appshortcuts/proguard-rules.pro b/appshortcuts/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/appshortcuts/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/appshortcuts/src/main/AndroidManifest.xml b/appshortcuts/src/main/AndroidManifest.xml new file mode 100644 index 00000000..7e42faf4 --- /dev/null +++ b/appshortcuts/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt new file mode 100644 index 00000000..3f70afa1 --- /dev/null +++ b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutRepository.kt @@ -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 + + +} + +internal class AppShortcutRepositoryImpl( + private val context: Context +): AppShortcutRepository { + override suspend fun getShortcutsForActivity( + launcherActivityInfo: LauncherActivityInfo, + count: Int, + ) = withContext(Dispatchers.IO){ + val launcherApps = context.getSystemService()!! + 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() + appShortcuts.addAll(shortcuts + ?.let { + if (it.size > count) it.subList(0, count) + else it + } + ?.map { + AppShortcut( + context, + it, + launcherActivityInfo.label.toString() + ) + } ?: emptyList()) + appShortcuts + } +} \ No newline at end of file diff --git a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt new file mode 100644 index 00000000..efc1274f --- /dev/null +++ b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/AppShortcutSerialization.kt @@ -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()!! + 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 + ) + } + } + } +} \ No newline at end of file diff --git a/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/Module.kt b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/Module.kt new file mode 100644 index 00000000..17fa8371 --- /dev/null +++ b/appshortcuts/src/main/java/de/mm20/launcher2/appshortcuts/Module.kt @@ -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 { AppShortcutRepositoryImpl(androidContext()) } +} \ No newline at end of file diff --git a/applications/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt b/appshortcuts/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt similarity index 90% rename from applications/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt rename to appshortcuts/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt index ed6e6609..56701e5d 100644 --- a/applications/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt +++ b/appshortcuts/src/main/java/de/mm20/launcher2/search/data/AppShortcut.kt @@ -6,17 +6,13 @@ import android.content.pm.LauncherApps import android.content.pm.ShortcutInfo import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.ColorDrawable -import android.os.Build import android.os.Bundle import android.os.Process -import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat 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.ktx.getSerialNumber -import de.mm20.launcher2.ktx.isAtLeastApiLevel -import de.mm20.launcher2.preferences.Settings import de.mm20.launcher2.preferences.Settings.IconSettings.LegacyIconBackground import kotlinx.coroutines.Dispatchers 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 icon = withContext(Dispatchers.IO) { launcherApps.getShortcutIconDrawable( diff --git a/badges/build.gradle.kts b/badges/build.gradle.kts index 301795e0..dfcc3b06 100644 --- a/badges/build.gradle.kts +++ b/badges/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(project(":ktx")) implementation(project(":applications")) + implementation(project(":appshortcuts")) implementation(project(":notifications")) implementation(project(":preferences")) implementation(project(":base")) diff --git a/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt b/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt index af3169b2..91299864 100644 --- a/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt +++ b/badges/src/main/java/de/mm20/launcher2/badges/providers/AppShortcutBadgeProvider.kt @@ -1,6 +1,5 @@ package de.mm20.launcher2.badges.providers -import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager import de.mm20.launcher2.badges.Badge diff --git a/favorites/build.gradle.kts b/favorites/build.gradle.kts index 9eccaece..d747a6c0 100644 --- a/favorites/build.gradle.kts +++ b/favorites/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { implementation(project(":database")) implementation(project(":preferences")) implementation(project(":applications")) + implementation(project(":appshortcuts")) implementation(project(":contacts")) implementation(project(":ktx")) implementation(project(":files")) diff --git a/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt b/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt index d1a24214..4d66cc4f 100644 --- a/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt +++ b/favorites/src/main/java/de/mm20/launcher2/favorites/Module.kt @@ -1,5 +1,7 @@ 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.CalendarEventSerializer 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.WikipediaSerializer import org.koin.android.ext.koin.androidContext -import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val favoritesModule = module { diff --git a/settings.gradle.kts b/settings.gradle.kts index aa3189d1..531a00a9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -384,3 +384,4 @@ dependencyResolutionManagement { } include(":notifications") include(":accounts") +include(":appshortcuts") diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index e672350f..5f459ebd 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -115,6 +115,7 @@ dependencies { implementation(project(":search")) implementation(project(":preferences")) implementation(project(":applications")) + implementation(project(":appshortcuts")) implementation(project(":calculator")) implementation(project(":files")) implementation(project(":widgets")) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt index 3326a1ea..f250ebf6 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt @@ -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 = shortcut.launcherShortcut.shortLabel ?: shortcut.launcherShortcut.longLabel diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItemVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItemVM.kt index 04f2823b..c5143351 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItemVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItemVM.kt @@ -10,14 +10,17 @@ import android.provider.Settings import android.service.notification.StatusBarNotification import androidx.core.content.FileProvider import androidx.core.content.getSystemService +import de.mm20.launcher2.appshortcuts.AppShortcutRepository import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.notifications.NotificationRepository import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.Application +import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import org.koin.core.component.inject @@ -26,6 +29,7 @@ class AppItemVM( private val app: Application ) : SearchableItemVM(app) { private val notificationRepository: NotificationRepository by inject() + private val appShortcutRepository: AppShortcutRepository by inject() val notifications = @@ -105,6 +109,12 @@ class AppItemVM( return launcherApps.getShortcutIconDrawable(shortcut, 0) } + val shortcuts = flow { + if (app is LauncherApp) { + emit(appShortcutRepository.getShortcutsForActivity(app.launcherActivityInfo, 5)) + } + } + fun isShortcutPinned(shortcut: AppShortcut): Flow { return favoritesRepository.isPinned(shortcut) }