From 9ac56626cb8467a89d5b36839935173799a28b6e Mon Sep 17 00:00:00 2001 From: sim Date: Fri, 15 Nov 2024 14:44:35 +0000 Subject: [PATCH] Migrate main activity to jetpack --- app/build.gradle.kts | 20 +- app/src/main/AndroidManifest.xml | 5 +- .../distributor/nextpush/EventBus.kt | 28 ++ .../nextpush/activities/AppAction.kt | 125 ++++++ .../nextpush/activities/AppListAdapter.kt | 95 ----- .../nextpush/activities/MainActivity.kt | 384 +++--------------- .../nextpush/activities/MainViewModel.kt | 93 +++++ .../nextpush/activities/UiAction.kt | 25 ++ .../nextpush/activities/ui/AppBarUi.kt | 134 ++++++ .../nextpush/activities/ui/MainUi.kt | 223 ++++++++++ .../nextpush/activities/ui/MainUiState.kt | 17 + .../activities/ui/NotificationChannelUi.kt | 93 +++++ .../activities/ui/RegistrationListState.kt | 22 + .../activities/ui/RegistrationState.kt | 45 ++ .../nextpush/activities/ui/UnregisterBarUi.kt | 113 ++++++ .../nextpush/activities/ui/theme/Color.kt | 221 ++++++++++ .../nextpush/activities/ui/theme/Theme.kt | 280 +++++++++++++ .../nextpush/activities/ui/theme/Type.kt | 5 + .../receivers/RegisterBroadcastReceiver.kt | 27 +- .../distributor/nextpush/utils/Clipboard.kt | 12 - .../nextpush/utils/Notifications.kt | 2 +- app/src/main/res/layout/activity_main.xml | 26 -- app/src/main/res/layout/content_main.xml | 94 ----- app/src/main/res/layout/item_app.xml | 21 - app/src/main/res/menu/menu_delete.xml | 13 - app/src/main/res/menu/menu_main.xml | 20 - app/src/main/res/values/strings.xml | 34 +- build.gradle.kts | 3 +- gradle/libs.versions.toml | 16 +- settings.gradle.kts | 1 + 30 files changed, 1548 insertions(+), 649 deletions(-) create mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/EventBus.kt create mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/activities/AppAction.kt delete mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/activities/AppListAdapter.kt create mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/activities/MainViewModel.kt create mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/activities/UiAction.kt create mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/activities/ui/AppBarUi.kt create mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/activities/ui/MainUi.kt create mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/activities/ui/MainUiState.kt create mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/activities/ui/NotificationChannelUi.kt create mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/activities/ui/RegistrationListState.kt create mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/activities/ui/RegistrationState.kt create mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/activities/ui/UnregisterBarUi.kt create mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/activities/ui/theme/Color.kt create mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/activities/ui/theme/Theme.kt create mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/activities/ui/theme/Type.kt delete mode 100644 app/src/main/java/org/unifiedpush/distributor/nextpush/utils/Clipboard.kt delete mode 100644 app/src/main/res/layout/activity_main.xml delete mode 100644 app/src/main/res/layout/content_main.xml delete mode 100644 app/src/main/res/layout/item_app.xml delete mode 100644 app/src/main/res/menu/menu_delete.xml delete mode 100644 app/src/main/res/menu/menu_main.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3cbbfd7..0d66924 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,13 +1,15 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) -} - -kotlin { - jvmToolchain(17) + alias(libs.plugins.compose.compiler) } android { + compileOptions { + targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_17 + } + compileSdk = 35 defaultConfig { @@ -18,6 +20,10 @@ android { versionName = "1.9.0" } + buildFeatures { + compose = true + } + buildTypes { getByName("release") { resValue("string", "app_name", "NextPush") @@ -57,10 +63,11 @@ if (project.hasProperty("sign")) { } } - dependencies { + implementation(libs.androidx.activity.compose) implementation(libs.androidx.constraintlayout) implementation(libs.androidx.coordinatorlayout) + implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.work.runtime.ktx) implementation(libs.appcompat) implementation(libs.kotlin.stdlib) @@ -72,4 +79,7 @@ dependencies { implementation(libs.retrofit.retrofit) implementation(libs.rxjava3.rxandroid) implementation(libs.rxjava3.rxjava) + implementation(libs.androidx.material3.android) + debugImplementation(libs.androidx.ui.tooling.preview.android) + debugImplementation(libs.androidx.ui.tooling) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 58683b8..3d5a3ae 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,11 +34,8 @@ - + - diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/EventBus.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/EventBus.kt new file mode 100644 index 0000000..7f7fcb1 --- /dev/null +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/EventBus.kt @@ -0,0 +1,28 @@ +package org.unifiedpush.distributor.nextpush + +import android.util.Log +import kotlin.coroutines.coroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.filterIsInstance + +object EventBus { + val mutEvents: MutableSharedFlow = MutableSharedFlow() + val events = mutEvents.asSharedFlow() + + suspend inline fun publish(event: T) { + if (mutEvents.subscriptionCount.value > 0) { + mutEvents.emit(event) + } + } + + suspend inline fun subscribe(crossinline onEvent: (T) -> Unit) { + events.filterIsInstance() + .collectLatest { event -> + coroutineContext.ensureActive() + onEvent(event) + } + } +} diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/AppAction.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/AppAction.kt new file mode 100644 index 0000000..ef3fe20 --- /dev/null +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/AppAction.kt @@ -0,0 +1,125 @@ +package org.unifiedpush.distributor.nextpush.activities + +import android.annotation.SuppressLint +import android.content.ActivityNotFoundException +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch +import org.unifiedpush.distributor.nextpush.AppStore +import org.unifiedpush.distributor.nextpush.EventBus +import org.unifiedpush.distributor.nextpush.LocalNotification +import org.unifiedpush.distributor.nextpush.account.AccountFactory +import org.unifiedpush.distributor.nextpush.distributor.Distributor +import org.unifiedpush.distributor.nextpush.distributor.Distributor.deleteApp +import org.unifiedpush.distributor.nextpush.distributor.Distributor.deleteDevice +import org.unifiedpush.distributor.nextpush.services.FailureHandler +import org.unifiedpush.distributor.nextpush.services.RestartWorker +import org.unifiedpush.distributor.nextpush.services.StartService +import org.unifiedpush.distributor.nextpush.utils.TAG + +class AppAction(private val type: Type, private val argv: Map? = null) { + enum class Type { + RestartService, + Logout, + AddChannel, + DisableBatteryOptimisation, + CopyEndpoint, + DeleteRegistration, + } + + fun handle(context: Context) { + when (type) { + Type.RestartService -> restartService(context) + Type.Logout -> logout(context) + Type.AddChannel -> addChannel(context, argv) + Type.DisableBatteryOptimisation -> disableBatteryOptimisation(context) + Type.CopyEndpoint -> copyEndpoint(context, argv) + Type.DeleteRegistration -> deleteRegistration(context, argv) + } + } + + private fun restartService(context: Context) { + Log.d(TAG, "Restarting the Listener") + FailureHandler.clearFails() + StartService.stopService { + RestartWorker.run(context, delay = 0) + } + } + + private fun logout(context: Context) { + deleteDevice(context) { + StartService.stopService() + FailureHandler.clearFails() + } + AccountFactory.logout(context) + AppStore(context).wipe() + UiAction.publish(UiAction.Type.Logout) + } + + private fun addChannel(context: Context, argv: Map?) { + (argv?.get(ARG_NEW_CHANNEL_TITLE) as String?)?.let { + LocalNotification.createChannel( + context, + it + ) { + Log.d(TAG, "Channel \"$it\" created") + UiAction.publish(UiAction.Type.UpdateRegistrations) + } + } + } + + @SuppressLint("BatteryLife") + private fun disableBatteryOptimisation(context: Context) { + Log.d(TAG, "Disabling battery optimization") + try { + context.startActivity( + Intent( + Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, + Uri.parse("package:${context.packageName}") + ) + ) + } catch (e: ActivityNotFoundException) { + try { + context.startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)) + } catch (e2: ActivityNotFoundException) { + context.startActivity(Intent(Settings.ACTION_SETTINGS)) + } + } + } + + private fun copyEndpoint(context: Context, argv: Map?) { + val token = argv?.get(ARG_TOKEN) as String? ?: return + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip: ClipData = ClipData.newPlainText( + "Endpoint", + Distributor.getEndpoint(context, token) + ) + clipboard.setPrimaryClip(clip) + } + + private fun deleteRegistration(context: Context, argv: Map?) { + val registrations = argv?.get(ARG_REGISTRATIONS) as List? ?: return + registrations?.forEach { + deleteApp(context, it) {} + } + } + + companion object { + const val ARG_NEW_CHANNEL_TITLE = "title" + const val ARG_TOKEN = "token" + const val ARG_REGISTRATIONS = "registrations" + } +} + +fun ViewModel.publishAction(action: AppAction) { + viewModelScope.launch { + EventBus.publish(action) + } +} diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/AppListAdapter.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/AppListAdapter.kt deleted file mode 100644 index 66d45b4..0000000 --- a/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/AppListAdapter.kt +++ /dev/null @@ -1,95 +0,0 @@ -package org.unifiedpush.distributor.nextpush.activities - -import android.content.Context -import android.util.SparseBooleanArray -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import android.widget.TextView -import androidx.core.view.isGone -import com.google.android.material.color.MaterialColors -import org.unifiedpush.distributor.nextpush.Database.Companion.getDb -import org.unifiedpush.distributor.nextpush.R -import org.unifiedpush.distributor.nextpush.utils.getApplicationName - -data class App( - val token: String, - val packageId: String -) - -class AppListAdapter(context: Context, private val resource: Int, apps: List) : ArrayAdapter(context, resource, apps) { - private var selectedItemsIds = SparseBooleanArray() - private val inflater = LayoutInflater.from(context) - private val db = getDb(context) - - private class ViewHolder { - var name: TextView? = null - var description: TextView? = null - } - - override fun getView(position: Int, pConvertView: View?, parent: ViewGroup): View { - var viewHolder: ViewHolder? = null - val convertView = pConvertView?.apply { - viewHolder = tag as ViewHolder - } ?: run { - val rConvertView = inflater.inflate(resource, parent, false) - viewHolder = ViewHolder().apply { - this.name = rConvertView.findViewById(R.id.item_app_name) as TextView - this.description = rConvertView.findViewById(R.id.item_description) as TextView - } - rConvertView.apply { - tag = viewHolder - } - } - getItem(position)?.let { - if (it.packageId == context.packageName) { - setViewHolderForLocalChannel(viewHolder, it) - } else { - setViewHolderForUnifiedPushApp(viewHolder, it) - } - } - if (selectedItemsIds.get(position)) { - convertView?.setBackgroundColor( - MaterialColors.getColor(convertView, com.google.android.material.R.attr.colorOnTertiary) - ) - } else { - convertView?.setBackgroundResource(0) - } - return convertView - } - - private fun setViewHolderForUnifiedPushApp(viewHolder: ViewHolder?, app: App) { - context.getApplicationName(app.packageId)?.let { - viewHolder?.name?.text = it - viewHolder?.description?.text = app.packageId - } ?: run { - viewHolder?.name?.text = app.packageId - viewHolder?.description?.isGone = true - } - } - - private fun setViewHolderForLocalChannel(viewHolder: ViewHolder?, app: App) { - val title = db.getNotificationTitle(app.token) - viewHolder?.name?.text = context.getString(R.string.local_notif_title).format(title) - viewHolder?.description?.text = context.getString(R.string.local_notif_description) - } - - fun toggleSelection(position: Int) { - selectView(position, !selectedItemsIds.get(position)) - } - - fun removeSelection() { - selectedItemsIds = SparseBooleanArray() - notifyDataSetChanged() - } - - private fun selectView(position: Int, value: Boolean) { - selectedItemsIds.put(position, value) - notifyDataSetChanged() - } - - fun getSelectedIds(): SparseBooleanArray { - return selectedItemsIds - } -} diff --git a/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/MainActivity.kt b/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/MainActivity.kt index c3931d0..8e6e343 100644 --- a/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/MainActivity.kt +++ b/app/src/main/java/org/unifiedpush/distributor/nextpush/activities/MainActivity.kt @@ -1,366 +1,82 @@ package org.unifiedpush.distributor.nextpush.activities -import android.annotation.SuppressLint -import android.content.ActivityNotFoundException -import android.content.ClipData -import android.content.ClipboardManager import android.content.Context import android.content.Intent -import android.net.Uri import android.os.Bundle -import android.os.PowerManager -import android.provider.Settings -import android.text.Html -import android.text.InputType -import android.text.SpannableStringBuilder import android.util.Log -import android.util.TypedValue -import android.view.ActionMode -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.widget.* // ktlint-disable no-wildcard-imports -import android.widget.AbsListView.MultiChoiceModeListener -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import androidx.core.util.size -import androidx.core.view.isGone -import androidx.core.view.setPadding -import com.google.android.material.card.MaterialCardView -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import org.unifiedpush.distributor.nextpush.AppStore -import org.unifiedpush.distributor.nextpush.Database.Companion.getDb -import org.unifiedpush.distributor.nextpush.LocalNotification -import org.unifiedpush.distributor.nextpush.R -import org.unifiedpush.distributor.nextpush.account.AccountFactory -import org.unifiedpush.distributor.nextpush.activities.PermissionsRequest.requestAppPermissions -import org.unifiedpush.distributor.nextpush.activities.StartActivity.Companion.goToStartActivity -import org.unifiedpush.distributor.nextpush.distributor.Distributor -import org.unifiedpush.distributor.nextpush.distributor.Distributor.deleteApp -import org.unifiedpush.distributor.nextpush.distributor.Distributor.deleteDevice -import org.unifiedpush.distributor.nextpush.services.FailureHandler +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.lifecycle.viewmodel.compose.viewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.unifiedpush.distributor.nextpush.EventBus +import org.unifiedpush.distributor.nextpush.activities.ui.MainUi +import org.unifiedpush.distributor.nextpush.activities.ui.theme.AppTheme import org.unifiedpush.distributor.nextpush.services.RestartWorker -import org.unifiedpush.distributor.nextpush.services.StartService import org.unifiedpush.distributor.nextpush.utils.TAG -import org.unifiedpush.distributor.nextpush.utils.copyToClipboard -import org.unifiedpush.distributor.nextpush.utils.getDebugInfo -import java.lang.String.format -class MainActivity : AppCompatActivity() { - - private lateinit var listView: ListView - - // if the unregister dialog is shown, we prevent the list to be reset - private var preventListReset = false - private var lastClickTime = 0L - private var clickCount = 0 +class MainActivity : ComponentActivity() { + private var viewModel: MainViewModel? = null + private var jobs: MutableList = emptyList().toMutableList() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - setSupportActionBar(findViewById(R.id.toolbar)) - this.requestAppPermissions() - if (AccountFactory.getAccount(this)?.connected != true) { - Log.d(TAG, "Not connected: going to StartActivity.") - goToStartActivity(this) - finish() - } - - findViewById(R.id.main_account_desc).text = - format(getString(R.string.main_account_desc), AccountFactory.getAccount(this)?.name) - invalidateOptionsMenu() RestartWorker.startPeriodic(this) - setDebugInformationListener() - findViewById(android.R.id.content)?.setOnApplyWindowInsetsListener { _, insets -> - val statusBarSize = insets.systemWindowInsetTop - findViewById(R.id.toolbar).setPadding(0, statusBarSize , 0, 0) - return@setOnApplyWindowInsetsListener insets + + setContent { + val viewModel = + viewModel { + MainViewModel(this@MainActivity) + }.also { + viewModel = it + } + AppTheme { + MainUi(viewModel) + } + subscribeActions() } } - override fun onStart() { - super.onStart() - showOptimisationWarning() - } - - override fun onWindowFocusChanged(hasFocus: Boolean) { - super.onWindowFocusChanged(hasFocus) - if (hasFocus) { - if (preventListReset) { - preventListReset = false - } else { - setListView() - } + private fun subscribeActions() { + Log.d(TAG, "Subscribing to actions") + jobs += CoroutineScope(Dispatchers.IO).launch { + EventBus.subscribe { it.handle(this@MainActivity) } } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_main, menu) - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - return when (item.itemId) { - R.id.action_restart -> { - restart() - return true - } - R.id.action_logout -> { - logout() - return true - } - R.id.action_add_local_channel -> { - addChannel() - return true - } - - else -> super.onOptionsItemSelected(item) - } - } - - @SuppressLint("BatteryLife") - private fun showOptimisationWarning() { - val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager - if (!powerManager.isIgnoringBatteryOptimizations(packageName)) { - findViewById(R.id.card_battery_optimization)?.isGone = false - findViewById