Skip to content

Commit

Permalink
Fixes & improvements
Browse files Browse the repository at this point in the history
- Remove "by widget" from reason_suspended string.
- Fix global Speak Emojis setting not reflected in app overrides.
- Prevent lateinit crash when opening App List.
- Prevent ANR when fetching app info.
- Change app isEnabled to non-nullable.
- Migrate to version catalog.
- Update Gradle and dependencies.
- Other fixes and code improvements.

Not targeting API 35 yet because it breaks audio focus in Android 15.
See: androidx/media#1815
  • Loading branch information
pilot51 committed Dec 16, 2024
1 parent ad15430 commit 771291a
Show file tree
Hide file tree
Showing 18 changed files with 359 additions and 323 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/auto-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '17'
java-version: '21'
distribution: 'temurin'
cache: 'gradle'

Expand Down
39 changes: 18 additions & 21 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import java.io.FileInputStream
import java.util.*

plugins {
id("com.android.application")
kotlin("android")
id("com.google.devtools.ksp")
alias(libs.plugins.android.application)
alias(libs.plugins.compose)
alias(libs.plugins.ksp)
}

val keystorePropertiesFile: File = rootProject.file("keystore.properties")
Expand Down Expand Up @@ -64,13 +65,9 @@ android {
compose = true
}

composeOptions {
kotlinCompilerExtensionVersion = "1.5.14"
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}

signingConfigs {
Expand Down Expand Up @@ -115,17 +112,17 @@ android {
}

dependencies {
implementation("androidx.core:core-ktx:1.13.1")
implementation("androidx.activity:activity-compose:1.9.0")
implementation("androidx.compose.material3:material3:1.3.0-beta01")
implementation("androidx.compose.material:material-icons-extended-android:1.6.7")
implementation("androidx.compose.ui:ui-tooling-preview:1.6.7")
debugImplementation("androidx.compose.ui:ui-tooling:1.6.7")
implementation("androidx.navigation:navigation-compose:2.7.7")
implementation("androidx.glance:glance-appwidget:1.0.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.room:room-ktx:2.6.1")
ksp("androidx.room:room-compiler:2.6.1")
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.24")
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
implementation(libs.accompanist.permissions)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material.iconsExtended)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.glance.appwidget)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.preference)
implementation(libs.androidx.room.ktx)
implementation(libs.kotlin.reflect)
ksp(libs.androidx.room.compiler)
debugImplementation(libs.androidx.compose.ui.tooling)
}
8 changes: 4 additions & 4 deletions app/schemas/com.pilot51.voicenotify.db.AppDatabase/2.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"formatVersion": 1,
"database": {
"version": 2,
"identityHash": "9fb9997d731f3b0c50e64706618b12c1",
"identityHash": "17d2b6035ddbc39718141ef47062f137",
"entities": [
{
"tableName": "apps",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package` TEXT NOT NULL, `name` TEXT NOT NULL COLLATE NOCASE, `is_enabled` INTEGER, PRIMARY KEY(`package`))",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`package` TEXT NOT NULL, `name` TEXT NOT NULL COLLATE NOCASE, `is_enabled` INTEGER NOT NULL, PRIMARY KEY(`package`))",
"fields": [
{
"fieldPath": "packageName",
Expand All @@ -24,7 +24,7 @@
"fieldPath": "isEnabled",
"columnName": "is_enabled",
"affinity": "INTEGER",
"notNull": false
"notNull": true
}
],
"primaryKey": {
Expand Down Expand Up @@ -208,7 +208,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9fb9997d731f3b0c50e64706618b12c1')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '17d2b6035ddbc39718141ef47062f137')"
]
}
}
13 changes: 8 additions & 5 deletions app/src/main/java/com/pilot51/voicenotify/AppListScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,12 @@ import com.pilot51.voicenotify.AppListViewModel.IgnoreType
import com.pilot51.voicenotify.db.App
import kotlinx.coroutines.delay

private lateinit var vmStoreOwner: ViewModelStoreOwner
private val vmStoreOwner = mutableStateOf<ViewModelStoreOwner?>(null)

@Composable
fun AppListActions() {
val vm: AppListViewModel = viewModel(vmStoreOwner)
val vmOwner by vmStoreOwner
val vm: AppListViewModel = viewModel(vmOwner ?: return)
var showSearchBar by remember { mutableStateOf(false) }
if (showSearchBar) {
val focusRequester = remember { FocusRequester() }
Expand Down Expand Up @@ -131,8 +132,9 @@ fun AppListActions() {
fun AppListScreen(
onConfigureApp: (app: App) -> Unit
) {
vmStoreOwner = LocalViewModelStoreOwner.current!!
val vm: AppListViewModel = viewModel(vmStoreOwner)
val vmOwner = LocalViewModelStoreOwner.current!!.also { vmStoreOwner.value = it }
DisposableEffect(vmOwner) { onDispose { vmStoreOwner.value = null } }
val vm: AppListViewModel = viewModel(vmOwner)
val packagesWithOverride by vm.packagesWithOverride
AppList(
vm.filteredApps,
Expand Down Expand Up @@ -164,6 +166,7 @@ private fun AppList(
}
}

@NonSkippableComposable
@Composable
private fun AppListItem(
app: App,
Expand Down Expand Up @@ -197,7 +200,7 @@ private fun AppListItem(
}
}
Checkbox(
checked = app.enabled,
checked = app.isEnabled,
modifier = Modifier.focusable(false),
onCheckedChange = { toggleIgnore(app) }
)
Expand Down
77 changes: 44 additions & 33 deletions app/src/main/java/com/pilot51/voicenotify/AppListViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.pilot51.voicenotify
import android.app.Application
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.compose.runtime.*
import androidx.lifecycle.AndroidViewModel
Expand All @@ -28,13 +29,11 @@ import com.pilot51.voicenotify.PreferenceHelper.KEY_APP_DEFAULT_ENABLE
import com.pilot51.voicenotify.PreferenceHelper.getPrefFlow
import com.pilot51.voicenotify.PreferenceHelper.setPref
import com.pilot51.voicenotify.db.App
import com.pilot51.voicenotify.db.AppDatabase
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import com.pilot51.voicenotify.db.AppDatabase.Companion.db
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.withLock
import kotlin.time.Duration.Companion.seconds

class AppListViewModel(application: Application) : AndroidViewModel(application) {
private val appContext = application.applicationContext
Expand All @@ -44,7 +43,7 @@ class AppListViewModel(application: Application) : AndroidViewModel(application)
private val syncAppsMutex by Common::syncAppsMutex
var searchQuery by mutableStateOf<String?>(null)
var showList by mutableStateOf(false)
private val settingsDao = AppDatabase.db.settingsDao
private val settingsDao = db.settingsDao
val packagesWithOverride @Composable get() =
settingsDao.packagesWithOverride().collectAsState(listOf())

Expand All @@ -65,47 +64,58 @@ class AppListViewModel(application: Application) : AndroidViewModel(application)
CoroutineScope(Dispatchers.IO).launch {
syncAppsMutex.withLock {
apps.clear()
apps.addAll(AppDatabase.db.appDao.getAll())
apps.addAll(db.appDao.getAll())
val isFirstLoad = apps.isEmpty()
val packMan = appContext.packageManager

// Remove uninstalled
for (a in apps.indices.reversed()) {
val app = apps[a]
try {
val installedApps = try {
withTimeoutInterruptible(10.seconds) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packMan.getApplicationInfo(app.packageName, PackageManager.ApplicationInfoFlags.of(0L))
packMan.getInstalledApplications(PackageManager.ApplicationInfoFlags.of(0L))
} else {
packMan.getApplicationInfo(app.packageName, 0)
packMan.getInstalledApplications(0)
}
} catch (e: PackageManager.NameNotFoundException) {
}
} catch (e: TimeoutCancellationException) {
Log.e(TAG, "Timed out fetching list of installed apps")
return@withLock
}

// Remove uninstalled
val appIter = apps.iterator()
for (app in appIter) {
if (installedApps.none { it.packageName == app.packageName }) {
if (!isFirstLoad) app.remove()
apps.removeAt(a)
appIter.remove()
}
}

// Add new
val installedApps = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packMan.getInstalledApplications(PackageManager.ApplicationInfoFlags.of(0L))
} else {
packMan.getInstalledApplications(0)
}
inst@ for (appInfo in installedApps) {
for (app in apps) {
if (app.packageName == appInfo.packageName) {
continue@inst
}
for (appInfo in installedApps) {
if (apps.any { it.packageName == appInfo.packageName }) {
continue
}
val label = try {
withTimeoutInterruptible(1.seconds) {
appInfo.loadLabel(packMan)
}.toString()
} catch (e: TimeoutCancellationException) {
Log.e(TAG, "Timed out fetching app label for package ${appInfo.packageName}")
continue
}
val app = App(
packageName = appInfo.packageName,
label = appInfo.loadLabel(packMan).toString(),
label = label,
isEnabled = appDefaultEnable
)
apps.add(app)
if (!isFirstLoad) app.updateDb()
}

// Sort list
apps.sortWith { app1, app2 -> app1.label.compareTo(app2.label, ignoreCase = true) }
if (isFirstLoad) AppDatabase.db.appDao.upsert(apps)

// Bulk add apps to DB if this is the first load
if (isFirstLoad) db.appDao.insert(apps)
}
isUpdating = false
filterApps()
Expand Down Expand Up @@ -136,22 +146,22 @@ class AppListViewModel(application: Application) : AndroidViewModel(application)
CoroutineScope(Dispatchers.IO).launch {
syncAppsMutex.withLock {
if (apps.isEmpty()) return@launch
for (app in apps) {
setIgnore(app, ignoreType)
apps.forEach {
setIgnore(it, ignoreType)
}
filterApps()
db.appDao.update(apps)
}
AppDatabase.db.appDao.upsert(apps)
}
}

fun setIgnore(app: App, ignoreType: IgnoreType) {
if (!app.enabled && (ignoreType == IGNORE_TOGGLE || ignoreType == IGNORE_NONE)) {
if (!app.isEnabled && (ignoreType == IGNORE_TOGGLE || ignoreType == IGNORE_NONE)) {
app.setEnabled(true, ignoreType == IGNORE_TOGGLE)
if (ignoreType == IGNORE_TOGGLE) {
Toast.makeText(appContext, appContext.getString(R.string.app_is_not_ignored, app.label), Toast.LENGTH_SHORT).show()
}
} else if (app.enabled && (ignoreType == IGNORE_TOGGLE || ignoreType == IGNORE_ALL)) {
} else if (app.isEnabled && (ignoreType == IGNORE_TOGGLE || ignoreType == IGNORE_ALL)) {
app.setEnabled(false, ignoreType == IGNORE_TOGGLE)
if (ignoreType == IGNORE_TOGGLE) {
Toast.makeText(appContext, appContext.getString(R.string.app_is_ignored, app.label), Toast.LENGTH_SHORT).show()
Expand All @@ -169,6 +179,7 @@ class AppListViewModel(application: Application) : AndroidViewModel(application)
}

companion object {
private val TAG = AppListViewModel::class.simpleName
/** The default enabled value for new apps. */
var appDefaultEnable =
runBlocking(Dispatchers.IO) {
Expand Down
60 changes: 33 additions & 27 deletions app/src/main/java/com/pilot51/voicenotify/Common.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,23 @@ package com.pilot51.voicenotify

import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.NameNotFoundException
import android.os.Build
import android.provider.Settings
import android.util.Pair
import android.util.Log
import androidx.compose.runtime.mutableStateListOf
import com.pilot51.voicenotify.AppListViewModel.Companion.appDefaultEnable
import com.pilot51.voicenotify.VNApplication.Companion.appContext
import com.pilot51.voicenotify.db.App
import com.pilot51.voicenotify.db.AppDatabase
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import com.pilot51.voicenotify.db.AppDatabase.Companion.db
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlin.time.Duration.Companion.seconds

object Common {
private val TAG = Common::class.simpleName

val notificationListenerSettingsIntent: Intent by lazy {
Intent(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
Expand All @@ -46,35 +49,38 @@ object Common {
* @param pkg Package name used to find [App] in current list or create a new one from system.
* @return Found or created [App], otherwise null if app not found on system.
*/
fun findOrAddApp(pkg: String): App? {
return runBlocking(Dispatchers.IO) {
syncAppsMutex.withLock {
if (apps.isEmpty()) {
apps.addAll(AppDatabase.db.appDao.getAll())
}
for (app in apps) {
if (app.packageName == pkg) {
return@runBlocking app
}
}
return@runBlocking try {
suspend fun findOrAddApp(pkg: String): App? {
syncAppsMutex.withLock {
if (apps.isEmpty()) {
apps.addAll(db.appDao.getAll())
}
apps.find { it.packageName == pkg }?.let {
return it
}
val appLabel = try {
withTimeoutInterruptible(2.seconds) {
val packMan = appContext.packageManager
val appInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
packMan.getApplicationInfo(pkg, PackageManager.ApplicationInfoFlags.of(0L))
} else {
packMan.getApplicationInfo(pkg, 0)
}.run {
loadLabel(packMan).toString()
}
val app = App(
packageName = pkg,
label = appInfo.loadLabel(packMan).toString(),
isEnabled = appDefaultEnable
)
apps.add(app.updateDb())
app
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
null
}
} catch (e: NameNotFoundException) {
Log.w(TAG, "App not found for package $pkg")
return null
} catch (e: TimeoutCancellationException) {
Log.e(TAG, "Timed out fetching app info/label for package $pkg")
return null
}
return App(
packageName = pkg,
label = appLabel,
isEnabled = appDefaultEnable
).apply {
apps.add(updateDb())
}
}
}
Expand Down
Loading

0 comments on commit 771291a

Please sign in to comment.