From 0413cb67b77c6382d0d96c3b56f1043bbc22d345 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Sun, 13 Oct 2024 00:41:05 +0500 Subject: [PATCH 01/40] add comment about completing self-update intent-based sessions --- .../impl/installer/session/IntentBasedInstallSession.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt index 3559867d9..b19a3baae 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt @@ -98,6 +98,14 @@ internal class IntentBasedInstallSession internal constructor( if (initialState.isTerminal) { return } + // Though it somewhat helps with self-update sessions, it's still faulty: + // if app is force-stopped while the session is committed (not confirmed) and in the meantime + // another installer updates the app, this session will be viewed as completed successfully. + // We can check that initiating installer package is the same as ours, but then if this session + // was successful, and before launching the app again it was updated by another installer, + // the session will be stuck as committed. Sadly, without centralized system + // sessions repository, such as android.content.pm.PackageInstaller, we can't reliably determine + // whether the intent-based Ackpine session was really successful. val isSuccessfulSelfUpdate = if (initialState is Committed && context.packageName == packageName) { getLastSelfUpdateTimestamp() > lastUpdateTimestamp } else { From 36c4097cda9dd8580283489a649771a2782ac433 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 18 Oct 2024 09:48:59 +0500 Subject: [PATCH 02/40] code style --- .../impl/installer/session/IntentBasedInstallSession.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt index b19a3baae..c22cc51cd 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt @@ -106,11 +106,9 @@ internal class IntentBasedInstallSession internal constructor( // the session will be stuck as committed. Sadly, without centralized system // sessions repository, such as android.content.pm.PackageInstaller, we can't reliably determine // whether the intent-based Ackpine session was really successful. - val isSuccessfulSelfUpdate = if (initialState is Committed && context.packageName == packageName) { - getLastSelfUpdateTimestamp() > lastUpdateTimestamp - } else { - false - } + val isSelfUpdate = initialState is Committed && context.packageName == packageName + val isLastUpdateTimestampUpdated = getLastSelfUpdateTimestamp() > lastUpdateTimestamp + val isSuccessfulSelfUpdate = isSelfUpdate && isLastUpdateTimestampUpdated if (isSuccessfulSelfUpdate && needToCompleteIfSucceeded) { complete(Succeeded) } From 4fc985e11ea44926b3dfcad6a8f6a0f402796029 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 18 Oct 2024 09:55:59 +0500 Subject: [PATCH 03/40] add comments to empty methods --- .../src/main/kotlin/ru/solrudev/ackpine/Ackpine.kt | 2 +- .../kotlin/ru/solrudev/ackpine/DisposableSubscription.kt | 4 ++-- .../impl/installer/session/SessionBasedInstallSession.kt | 8 ++++---- .../ru/solrudev/ackpine/impl/session/AbstractSession.kt | 6 +++--- .../main/kotlin/ru/solrudev/ackpine/session/Session.kt | 8 ++++---- .../solrudev/ackpine/sample/install/InstallFragment.java | 2 +- .../solrudev/ackpine/sample/install/InstallViewModel.java | 4 ++-- .../ackpine/sample/uninstall/UninstallViewModel.java | 4 ++-- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/Ackpine.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/Ackpine.kt index 49aaf2661..c01898c64 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/Ackpine.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/Ackpine.kt @@ -39,7 +39,7 @@ public object Ackpine { private val configurationChangesCallback = object : ComponentCallbacks { override fun onConfigurationChanged(newConfig: Configuration) = createNotificationChannel() - override fun onLowMemory() {} + override fun onLowMemory() { /* noop */ } } /** diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/DisposableSubscription.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/DisposableSubscription.kt index 6f80064d3..87ba44bb0 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/DisposableSubscription.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/DisposableSubscription.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Ilya Fomichev + * Copyright (C) 2023-2024 Ilya Fomichev * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -75,5 +75,5 @@ public class DisposableSubscriptionContainer : DisposableSubscription { @RestrictTo(RestrictTo.Scope.LIBRARY) internal data object DummyDisposableSubscription : DisposableSubscription { override val isDisposed: Boolean = true - override fun dispose() {} + override fun dispose() { /* noop */ } } \ No newline at end of file diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt index c1d914e71..13c07c8ca 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt @@ -273,10 +273,10 @@ internal class SessionBasedInstallSession internal constructor( } private fun packageInstallerSessionCallback(nativeSessionId: Int) = object : PackageInstaller.SessionCallback() { - override fun onCreated(sessionId: Int) {} - override fun onBadgingChanged(sessionId: Int) {} - override fun onActiveChanged(sessionId: Int, active: Boolean) {} - override fun onFinished(sessionId: Int, success: Boolean) {} + override fun onCreated(sessionId: Int) { /* noop */ } + override fun onBadgingChanged(sessionId: Int) { /* noop */ } + override fun onActiveChanged(sessionId: Int, active: Boolean) { /* noop */ } + override fun onFinished(sessionId: Int, success: Boolean) { /* noop */ } override fun onProgressChanged(sessionId: Int, progress: Float) { if (sessionId == nativeSessionId) { diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/AbstractSession.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/AbstractSession.kt index 85be5e503..663f6089d 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/AbstractSession.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/AbstractSession.kt @@ -119,7 +119,7 @@ internal abstract class AbstractSession protected constructor( * Release any held resources after session's completion or cancellation. Processing in this method should be * lightweight. */ - protected open fun doCleanup() {} + protected open fun doCleanup() { /* optional */ } /** * Notifies that preparations are done and sets session's state to [Awaiting]. @@ -133,13 +133,13 @@ internal abstract class AbstractSession protected constructor( * This callback method is invoked when the session's been committed. Processing in this method should be * lightweight. */ - protected open fun onCommitted() {} + protected open fun onCommitted() { /* optional */ } /** * This callback method is invoked when the session's been [completed][Session.isCompleted]. Processing in * this method should be lightweight. */ - protected open fun onCompleted(success: Boolean) {} + protected open fun onCompleted(success: Boolean) { /* optional */ } final override fun launch(): Boolean { if (isPreparing || isCancelling.get()) { diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/Session.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/Session.kt index eabd01386..6b1246c83 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/Session.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/Session.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Ilya Fomichev + * Copyright (C) 2023-2024 Ilya Fomichev * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -208,20 +208,20 @@ public interface Session { * Notifies that session was completed successfully. * @param sessionId ID of the session which had its state updated. */ - public open fun onSuccess(sessionId: UUID) {} + public open fun onSuccess(sessionId: UUID) { /* to be overridden */ } /** * Notifies that session was completed with an error. * @param sessionId ID of the session which had its state updated. * @param failure session's failure cause. */ - public open fun onFailure(sessionId: UUID, failure: F) {} + public open fun onFailure(sessionId: UUID, failure: F) { /* to be overridden */ } /** * Notifies that session was cancelled. * @param sessionId ID of the session which had its state updated. */ - public open fun onCancelled(sessionId: UUID) {} + public open fun onCancelled(sessionId: UUID) { /* to be overridden */ } final override fun onStateChanged(sessionId: UUID, state: State) { if (state.isTerminal) { diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallFragment.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallFragment.java index dc6fd030e..9f954dc7d 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallFragment.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallFragment.java @@ -131,7 +131,7 @@ private void onInstallButtonClick() { private void chooseFile() { try { pickerLauncher.launch("*/*"); - } catch (ActivityNotFoundException ignored) { + } catch (ActivityNotFoundException ignored) { // noop } } diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java index 3f792bff9..ec361b091 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java @@ -126,7 +126,7 @@ public void onSuccess(@Nullable ProgressSession session) { } @Override - public void onFailure(@NonNull Throwable t) { + public void onFailure(@NonNull Throwable t) { // noop } }, MoreExecutors.directExecutor()); } @@ -159,7 +159,7 @@ public void onSuccess(@Nullable ProgressSession session) { } @Override - public void onFailure(@NonNull Throwable t) { + public void onFailure(@NonNull Throwable t) { // noop } }, MoreExecutors.directExecutor()); } diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.java index 64e3cce30..e2f172036 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.java @@ -138,7 +138,7 @@ public void onSuccess(@Nullable Session session) { } @Override - public void onFailure(@NonNull Throwable t) { + public void onFailure(@NonNull Throwable t) { // noop } }, MoreExecutors.directExecutor()); } @@ -153,7 +153,7 @@ public void onSuccess(@Nullable Session session) { } @Override - public void onFailure(@NonNull Throwable t) { + public void onFailure(@NonNull Throwable t) { // noop } }, MoreExecutors.directExecutor()); } From cf4aee4060d40b714b2d198520350adb15e723a2 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 18 Oct 2024 09:59:49 +0500 Subject: [PATCH 04/40] reorder java modifiers --- .../ackpine/sample/install/InstallSessionsAdapter.java | 10 +++++----- .../ackpine/sample/uninstall/ApplicationsAdapter.java | 6 +++--- .../ackpine/sample/uninstall/UninstallViewModel.java | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java index 1116573af..dc3ee45cc 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java @@ -45,8 +45,8 @@ public final class InstallSessionsAdapter extends ListAdapter { - private final static SessionDiffCallback DIFF_CALLBACK = new SessionDiffCallback(); - private final static ItemAnimator ITEM_ANIMATOR = new ItemAnimator(); + private static final SessionDiffCallback DIFF_CALLBACK = new SessionDiffCallback(); + private static final ItemAnimator ITEM_ANIMATOR = new ItemAnimator(); private final Consumer onCancelClick; private final Consumer onItemSwipe; private final Handler handler = new Handler(Looper.getMainLooper()); @@ -59,7 +59,7 @@ public InstallSessionsAdapter(Consumer onCancelClick, Consumer onIte this.onItemSwipe = onItemSwipe; } - public final static class SessionViewHolder extends RecyclerView.ViewHolder { + public static final class SessionViewHolder extends RecyclerView.ViewHolder { private final ItemInstallSessionBinding binding; private final Consumer onClick; @@ -188,7 +188,7 @@ private void notifyProgressChanged(@NonNull List progress) { private record ProgressUpdate(Progress progress, boolean animate) { } - private final static class SessionDiffCallback extends DiffUtil.ItemCallback { + private static final class SessionDiffCallback extends DiffUtil.ItemCallback { @Override public boolean areItemsTheSame(@NonNull SessionData oldItem, @NonNull SessionData newItem) { @@ -201,7 +201,7 @@ public boolean areContentsTheSame(@NonNull SessionData oldItem, @NonNull Session } } - private final static class ItemAnimator extends DefaultItemAnimator { + private static final class ItemAnimator extends DefaultItemAnimator { @Override public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) { diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/ApplicationsAdapter.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/ApplicationsAdapter.java index f939eed6b..0af873f8d 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/ApplicationsAdapter.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/ApplicationsAdapter.java @@ -29,7 +29,7 @@ public final class ApplicationsAdapter extends ListAdapter { - private final static ApplicationDiffCallback DIFF_CALLBACK = new ApplicationDiffCallback(); + private static final ApplicationDiffCallback DIFF_CALLBACK = new ApplicationDiffCallback(); private final Consumer onClick; public ApplicationsAdapter(Consumer onClick) { @@ -37,7 +37,7 @@ public ApplicationsAdapter(Consumer onClick) { this.onClick = onClick; } - public final static class ApplicationViewHolder extends RecyclerView.ViewHolder { + public static final class ApplicationViewHolder extends RecyclerView.ViewHolder { private final ItemApplicationBinding binding; private final Consumer onClick; @@ -73,7 +73,7 @@ public void onBindViewHolder(@NonNull ApplicationViewHolder holder, int position holder.bind(applicationData); } - private final static class ApplicationDiffCallback extends DiffUtil.ItemCallback { + private static final class ApplicationDiffCallback extends DiffUtil.ItemCallback { @Override public boolean areItemsTheSame(@NonNull ApplicationData oldItem, @NonNull ApplicationData newItem) { return oldItem.packageName().equals(newItem.packageName()); diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.java index e2f172036..0b66b511e 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.java @@ -51,8 +51,8 @@ public final class UninstallViewModel extends ViewModel { - private final static String SESSION_ID_KEY = "SESSION_ID"; - private final static String PACKAGE_NAME_KEY = "PACKAGE_NAME"; + private static final String SESSION_ID_KEY = "SESSION_ID"; + private static final String PACKAGE_NAME_KEY = "PACKAGE_NAME"; private final MutableLiveData isLoading = new MutableLiveData<>(false); private final MutableLiveData> applications = new MutableLiveData<>(new ArrayList<>()); private final DisposableSubscriptionContainer subscriptions = new DisposableSubscriptionContainer(); From 6aa70009dcfadae9021b8cd8450829ca1938623d Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 18 Oct 2024 10:14:58 +0500 Subject: [PATCH 05/40] replace if with check precondition --- .../kotlin/ru/solrudev/ackpine/splits/CloseableSequence.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/CloseableSequence.kt b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/CloseableSequence.kt index 23247e77a..cc17458fe 100644 --- a/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/CloseableSequence.kt +++ b/ackpine-splits/src/main/kotlin/ru/solrudev/ackpine/splits/CloseableSequence.kt @@ -88,8 +88,8 @@ private class CloseableSequenceImpl( ) override fun iterator(): Iterator { - if (!isConsumed.compareAndSet(false, true)) { - throw IllegalStateException("This sequence can be consumed only once.") + check(isConsumed.compareAndSet(false, true)) { + "This sequence can be consumed only once." } return iterator { scope = this From 1f713c16b8898786bacc180c5da49dfd18938dc2 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 18 Oct 2024 10:45:08 +0500 Subject: [PATCH 06/40] add type check to equals() --- .../ackpine/installer/parameters/InstallParameters.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/installer/parameters/InstallParameters.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/installer/parameters/InstallParameters.kt index dcb40d534..609dd09c2 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/installer/parameters/InstallParameters.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/installer/parameters/InstallParameters.kt @@ -325,7 +325,14 @@ private class RealMutableApkList : MutableApkList { } override fun toList() = apks.toList() - override fun equals(other: Any?) = apks == other + + override fun equals(other: Any?): Boolean { + if (other !is RealMutableApkList) { + return false + } + return apks == other + } + override fun hashCode() = apks.hashCode() override fun toString() = "ApkList($apks)" From 1474e26103cad87524be8f3805531367d3e4009b Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 18 Oct 2024 10:59:19 +0500 Subject: [PATCH 07/40] add comments to empty blocks --- .../impl/installer/session/SessionBasedInstallSession.kt | 2 +- .../ru/solrudev/ackpine/sample/install/InstallFragment.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt index 13c07c8ca..784e66c04 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt @@ -288,7 +288,7 @@ internal class SessionBasedInstallSession internal constructor( private fun abandonSession() { try { packageInstaller.abandonSession(nativeSessionId) - } catch (_: Throwable) { + } catch (_: Throwable) { // noop } } diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallFragment.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallFragment.kt index 3976085a9..b934510a0 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallFragment.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallFragment.kt @@ -109,7 +109,7 @@ class InstallFragment : Fragment(R.layout.fragment_install) { private fun chooseFile() { try { pickerLauncher.launch("*/*") - } catch (_: ActivityNotFoundException) { + } catch (_: ActivityNotFoundException) { // noop } } From a20ac8c2dce25a41eb2b03702c8999fadd674590 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 18 Oct 2024 13:52:27 +0500 Subject: [PATCH 08/40] fix repeated invocation of awaitSessionFromSavedState flow body on each collect --- .../sample/uninstall/UninstallViewModel.kt | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.kt index 2dd0414f8..a4a62dd27 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.kt @@ -27,8 +27,9 @@ import androidx.lifecycle.viewmodel.CreationExtras import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runInterruptible @@ -50,15 +51,11 @@ class UninstallViewModel( private val savedStateHandle: SavedStateHandle ) : ViewModel() { - private val awaitSessionFromSavedState = flow { - val sessionId = savedStateHandle.get(SESSION_ID_KEY) - if (sessionId != null) { - packageUninstaller.getSession(sessionId)?.let(::awaitSession) - } - } - private val _uiState = MutableStateFlow(UninstallUiState()) - val uiState = merge(awaitSessionFromSavedState, _uiState) + + val uiState = _uiState + .onStart { awaitSessionFromSavedState() } + .stateIn(viewModelScope, SharingStarted.Lazily, UninstallUiState()) fun loadApplications(refresh: Boolean, applicationsFactory: () -> List) { if (!refresh && _uiState.value.applications.isNotEmpty()) { @@ -86,6 +83,13 @@ class UninstallViewModel( _uiState.update { it.copy(applications = applications) } } + private fun awaitSessionFromSavedState() = viewModelScope.launch { + val sessionId = savedStateHandle.get(SESSION_ID_KEY) + if (sessionId != null) { + packageUninstaller.getSession(sessionId)?.let(::awaitSession) + } + } + private fun clearSavedState() { savedStateHandle.remove(SESSION_ID_KEY) savedStateHandle.remove(PACKAGE_NAME_KEY) From 145e86953faf392b20428646e58c51dc65ec0b2b Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 18 Oct 2024 13:53:00 +0500 Subject: [PATCH 09/40] use onStart() flow operator --- .../sample/install/InstallViewModel.kt | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt index 21ac14c56..6153b2b31 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt @@ -30,11 +30,10 @@ import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.runInterruptible @@ -62,30 +61,16 @@ class InstallViewModel( private val sessionDataRepository: SessionDataRepository ) : ViewModel() { - private val awaitSessionsFromSavedState = channelFlow { - val sessions = sessionDataRepository.sessions.value - if (sessions.isNotEmpty()) { - sessions - .map { sessionData -> - async { packageInstaller.getSession(sessionData.id) } - } - .awaitAll() - .filterNotNull() - .forEach(::awaitSession) - } - } - private val error = MutableStateFlow(NotificationString.empty()) - val uiState = merge( - awaitSessionsFromSavedState, - combine( - error, - sessionDataRepository.sessions, - sessionDataRepository.sessionsProgress, - ::InstallUiState - ) - ).stateIn(viewModelScope, SharingStarted.Lazily, InstallUiState()) + val uiState = combine( + error, + sessionDataRepository.sessions, + sessionDataRepository.sessionsProgress, + ::InstallUiState + ) + .onStart { awaitSessionsFromSavedState() } + .stateIn(viewModelScope, SharingStarted.Lazily, InstallUiState()) fun installPackage(apks: Sequence, fileName: String) = viewModelScope.launch { val uris = runInterruptible(Dispatchers.IO) { apks.toUrisList() } @@ -111,6 +96,19 @@ class InstallViewModel( error.value = NotificationString.empty() } + private fun awaitSessionsFromSavedState() = viewModelScope.launch { + val sessions = this@InstallViewModel.sessionDataRepository.sessions.value + if (sessions.isNotEmpty()) { + sessions + .map { sessionData -> + async { this@InstallViewModel.packageInstaller.getSession(sessionData.id) } + } + .awaitAll() + .filterNotNull() + .forEach(::awaitSession) + } + } + private fun awaitSession(session: ProgressSession) = viewModelScope.launch { session.progress .onEach { progress -> sessionDataRepository.updateSessionProgress(session.id, progress) } From 89e6b70aabe91ac5659c774f0ff0a6e5f7a0434c Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Tue, 22 Oct 2024 13:20:54 +0500 Subject: [PATCH 10/40] don't block calling thread in IntentBasedInstallSession's init block --- .../impl/installer/session/IntentBasedInstallSession.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt index c22cc51cd..8ffea76e8 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt @@ -27,7 +27,6 @@ import androidx.core.content.FileProvider import androidx.core.net.toUri import ru.solrudev.ackpine.AckpineFileProvider import ru.solrudev.ackpine.helpers.concurrent.BinarySemaphore -import ru.solrudev.ackpine.helpers.concurrent.executeWithSemaphore import ru.solrudev.ackpine.helpers.concurrent.withPermit import ru.solrudev.ackpine.impl.database.dao.LastUpdateTimestampDao import ru.solrudev.ackpine.impl.database.dao.SessionDao @@ -113,8 +112,10 @@ internal class IntentBasedInstallSession internal constructor( complete(Succeeded) } if (isSuccessfulSelfUpdate) { - executor.executeWithSemaphore(dbWriteSemaphore) { - lastUpdateTimestampDao.setLastUpdateTimestamp(id.toString(), getLastSelfUpdateTimestamp()) + executor.execute { + dbWriteSemaphore.withPermit { + lastUpdateTimestampDao.setLastUpdateTimestamp(id.toString(), getLastSelfUpdateTimestamp()) + } } } } From aac5f3dd0e9b7734e4d26563a130cae53f9f5b7f Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Wed, 23 Oct 2024 15:54:15 +0500 Subject: [PATCH 11/40] use last payload object to update items progress --- .../solrudev/ackpine/sample/install/InstallSessionsAdapter.java | 2 +- .../solrudev/ackpine/sample/install/InstallSessionsAdapter.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java index dc3ee45cc..a6f168c53 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java @@ -162,7 +162,7 @@ public void onBindViewHolder(@NonNull SessionViewHolder holder, int position, @N } holder.bind(sessionData); if (!payloads.isEmpty()) { - final var progressUpdate = (ProgressUpdate) payloads.get(0); + final var progressUpdate = (ProgressUpdate) payloads.get(payloads.size() - 1); holder.setProgress(progressUpdate.progress(), progressUpdate.animate()); } } diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.kt index 1d98f092e..415ec4626 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.kt @@ -133,7 +133,7 @@ class InstallSessionsAdapter( } holder.bind(sessionData) if (payloads.isNotEmpty()) { - val progressUpdate = payloads.first() as ProgressUpdate + val progressUpdate = payloads.last() as ProgressUpdate holder.setProgress(progressUpdate.progress, progressUpdate.animate) } } From a824b40e8bd56714cd914ec56fca0f6eb0397be0 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Wed, 23 Oct 2024 16:21:51 +0500 Subject: [PATCH 12/40] noop -> no-op --- .../src/main/kotlin/ru/solrudev/ackpine/Ackpine.kt | 2 +- .../ru/solrudev/ackpine/DisposableSubscription.kt | 2 +- .../installer/session/SessionBasedInstallSession.kt | 10 +++++----- .../ackpine/sample/install/InstallFragment.java | 2 +- .../ackpine/sample/install/InstallViewModel.java | 4 ++-- .../ackpine/sample/uninstall/UninstallViewModel.java | 4 ++-- .../solrudev/ackpine/sample/install/InstallFragment.kt | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/Ackpine.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/Ackpine.kt index c01898c64..7307c0774 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/Ackpine.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/Ackpine.kt @@ -39,7 +39,7 @@ public object Ackpine { private val configurationChangesCallback = object : ComponentCallbacks { override fun onConfigurationChanged(newConfig: Configuration) = createNotificationChannel() - override fun onLowMemory() { /* noop */ } + override fun onLowMemory() { /* no-op */ } } /** diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/DisposableSubscription.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/DisposableSubscription.kt index 87ba44bb0..51faceffa 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/DisposableSubscription.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/DisposableSubscription.kt @@ -75,5 +75,5 @@ public class DisposableSubscriptionContainer : DisposableSubscription { @RestrictTo(RestrictTo.Scope.LIBRARY) internal data object DummyDisposableSubscription : DisposableSubscription { override val isDisposed: Boolean = true - override fun dispose() { /* noop */ } + override fun dispose() { /* no-op */ } } \ No newline at end of file diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt index 784e66c04..598b03168 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt @@ -273,10 +273,10 @@ internal class SessionBasedInstallSession internal constructor( } private fun packageInstallerSessionCallback(nativeSessionId: Int) = object : PackageInstaller.SessionCallback() { - override fun onCreated(sessionId: Int) { /* noop */ } - override fun onBadgingChanged(sessionId: Int) { /* noop */ } - override fun onActiveChanged(sessionId: Int, active: Boolean) { /* noop */ } - override fun onFinished(sessionId: Int, success: Boolean) { /* noop */ } + override fun onCreated(sessionId: Int) { /* no-op */ } + override fun onBadgingChanged(sessionId: Int) { /* no-op */ } + override fun onActiveChanged(sessionId: Int, active: Boolean) { /* no-op */ } + override fun onFinished(sessionId: Int, success: Boolean) { /* no-op */ } override fun onProgressChanged(sessionId: Int, progress: Float) { if (sessionId == nativeSessionId) { @@ -288,7 +288,7 @@ internal class SessionBasedInstallSession internal constructor( private fun abandonSession() { try { packageInstaller.abandonSession(nativeSessionId) - } catch (_: Throwable) { // noop + } catch (_: Throwable) { // no-op } } diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallFragment.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallFragment.java index 9f954dc7d..99334926c 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallFragment.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallFragment.java @@ -131,7 +131,7 @@ private void onInstallButtonClick() { private void chooseFile() { try { pickerLauncher.launch("*/*"); - } catch (ActivityNotFoundException ignored) { // noop + } catch (ActivityNotFoundException ignored) { // no-op } } diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java index ec361b091..38731ceed 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java @@ -126,7 +126,7 @@ public void onSuccess(@Nullable ProgressSession session) { } @Override - public void onFailure(@NonNull Throwable t) { // noop + public void onFailure(@NonNull Throwable t) { // no-op } }, MoreExecutors.directExecutor()); } @@ -159,7 +159,7 @@ public void onSuccess(@Nullable ProgressSession session) { } @Override - public void onFailure(@NonNull Throwable t) { // noop + public void onFailure(@NonNull Throwable t) { // no-op } }, MoreExecutors.directExecutor()); } diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.java index 0b66b511e..1b5d92051 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/uninstall/UninstallViewModel.java @@ -138,7 +138,7 @@ public void onSuccess(@Nullable Session session) { } @Override - public void onFailure(@NonNull Throwable t) { // noop + public void onFailure(@NonNull Throwable t) { // no-op } }, MoreExecutors.directExecutor()); } @@ -153,7 +153,7 @@ public void onSuccess(@Nullable Session session) { } @Override - public void onFailure(@NonNull Throwable t) { // noop + public void onFailure(@NonNull Throwable t) { // no-op } }, MoreExecutors.directExecutor()); } diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallFragment.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallFragment.kt index b934510a0..992641684 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallFragment.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallFragment.kt @@ -109,7 +109,7 @@ class InstallFragment : Fragment(R.layout.fragment_install) { private fun chooseFile() { try { pickerLauncher.launch("*/*") - } catch (_: ActivityNotFoundException) { // noop + } catch (_: ActivityNotFoundException) { // no-op } } From 6fb608ca71e979b776e8dcea4cd91c05f53b9545 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Wed, 23 Oct 2024 16:32:16 +0500 Subject: [PATCH 13/40] expose progress setter as method in AbstractProgressSession --- .../installer/session/IntentBasedInstallSession.kt | 8 ++++---- .../installer/session/SessionBasedInstallSession.kt | 6 +++--- .../impl/installer/session/helpers/IoHelpers.kt | 10 +++++----- .../ackpine/impl/session/AbstractProgressSession.kt | 7 ++++++- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt index 8ffea76e8..cbb1eba41 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/IntentBasedInstallSession.kt @@ -33,7 +33,7 @@ import ru.solrudev.ackpine.impl.database.dao.SessionDao import ru.solrudev.ackpine.impl.database.dao.SessionFailureDao import ru.solrudev.ackpine.impl.database.dao.SessionProgressDao import ru.solrudev.ackpine.impl.installer.activity.IntentBasedInstallActivity -import ru.solrudev.ackpine.impl.installer.session.helpers.STREAM_COPY_PROGRESS_MAX +import ru.solrudev.ackpine.impl.installer.session.helpers.PROGRESS_MAX import ru.solrudev.ackpine.impl.installer.session.helpers.copyTo import ru.solrudev.ackpine.impl.installer.session.helpers.openAssetFileDescriptor import ru.solrudev.ackpine.impl.session.AbstractProgressSession @@ -163,12 +163,12 @@ internal class IntentBasedInstallSession internal constructor( } override fun onCommitted() { - progress = Progress((STREAM_COPY_PROGRESS_MAX * 0.9).roundToInt(), STREAM_COPY_PROGRESS_MAX) + setProgress((PROGRESS_MAX * 0.9).roundToInt()) } override fun onCompleted(success: Boolean) { if (success) { - progress = Progress(STREAM_COPY_PROGRESS_MAX, STREAM_COPY_PROGRESS_MAX) + setProgress(PROGRESS_MAX) } } @@ -194,7 +194,7 @@ internal class IntentBasedInstallSession internal constructor( var currentProgress = 0 apkStream.copyTo(bufferedOutputStream, afd.declaredLength, cancellationSignal, onProgress = { delta -> currentProgress += delta - progress = Progress((currentProgress * 0.8).roundToInt(), STREAM_COPY_PROGRESS_MAX) + setProgress((currentProgress * 0.8).roundToInt()) }) bufferedOutputStream.flush() outputStream.fd.sync() diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt index 598b03168..767a00157 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt @@ -42,7 +42,7 @@ import ru.solrudev.ackpine.impl.database.dao.SessionDao import ru.solrudev.ackpine.impl.database.dao.SessionFailureDao import ru.solrudev.ackpine.impl.database.dao.SessionProgressDao import ru.solrudev.ackpine.impl.installer.activity.SessionBasedInstallCommitActivity -import ru.solrudev.ackpine.impl.installer.session.helpers.STREAM_COPY_PROGRESS_MAX +import ru.solrudev.ackpine.impl.installer.session.helpers.PROGRESS_MAX import ru.solrudev.ackpine.impl.installer.session.helpers.copyTo import ru.solrudev.ackpine.impl.installer.session.helpers.openAssetFileDescriptor import ru.solrudev.ackpine.impl.session.AbstractProgressSession @@ -209,7 +209,7 @@ internal class SessionBasedInstallSession internal constructor( val future = ResolvableFuture.create() val countdown = AtomicInteger(apks.size) val currentProgress = AtomicInteger(0) - val progressMax = apks.size * STREAM_COPY_PROGRESS_MAX + val progressMax = apks.size * PROGRESS_MAX apks.forEachIndexed { index, uri -> val afd = context.openAssetFileDescriptor(uri, cancellationSignal) ?: error("AssetFileDescriptor was null: $uri") @@ -280,7 +280,7 @@ internal class SessionBasedInstallSession internal constructor( override fun onProgressChanged(sessionId: Int, progress: Float) { if (sessionId == nativeSessionId) { - this@SessionBasedInstallSession.progress = Progress((progress * 100).toInt(), 100) + setProgress((progress * PROGRESS_MAX).toInt()) } } } diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/helpers/IoHelpers.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/helpers/IoHelpers.kt index 73b3bf88a..d658e738c 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/helpers/IoHelpers.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/helpers/IoHelpers.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Ilya Fomichev + * Copyright (C) 2023-2024 Ilya Fomichev * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import kotlin.math.roundToInt private const val BUFFER_LENGTH = 8192 @get:JvmSynthetic -internal const val STREAM_COPY_PROGRESS_MAX: Int = 100 +internal const val PROGRESS_MAX: Int = 100 @JvmSynthetic internal inline fun InputStream.copyTo( @@ -33,7 +33,7 @@ internal inline fun InputStream.copyTo( signal: CancellationSignal, onProgress: (Int) -> Unit = {} ) { - val progressRatio = (size.toDouble() / (BUFFER_LENGTH * STREAM_COPY_PROGRESS_MAX)).roundToInt().coerceAtLeast(1) + val progressRatio = (size.toDouble() / (BUFFER_LENGTH * PROGRESS_MAX)).roundToInt().coerceAtLeast(1) val buffer = ByteArray(BUFFER_LENGTH) var currentProgress = 0 var accumulatedBytesRead = 0 @@ -50,11 +50,11 @@ internal inline fun InputStream.copyTo( accumulatedBytesRead = 0 val progress = ++currentProgress / progressRatio val shouldEmitProgress = currentProgress - (progress * progressRatio) == 0 - if (shouldEmitProgress && progress <= STREAM_COPY_PROGRESS_MAX) { + if (shouldEmitProgress && progress <= PROGRESS_MAX) { progressEmitCounter++ onProgress(1) } } } - onProgress(STREAM_COPY_PROGRESS_MAX - progressEmitCounter) + onProgress(PROGRESS_MAX - progressEmitCounter) } \ No newline at end of file diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/AbstractProgressSession.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/AbstractProgressSession.kt index f62439745..9e3a26379 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/AbstractProgressSession.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/AbstractProgressSession.kt @@ -26,6 +26,7 @@ import ru.solrudev.ackpine.helpers.concurrent.BinarySemaphore import ru.solrudev.ackpine.impl.database.dao.SessionDao import ru.solrudev.ackpine.impl.database.dao.SessionFailureDao import ru.solrudev.ackpine.impl.database.dao.SessionProgressDao +import ru.solrudev.ackpine.impl.installer.session.helpers.PROGRESS_MAX import ru.solrudev.ackpine.session.Failure import ru.solrudev.ackpine.session.Progress import ru.solrudev.ackpine.session.ProgressSession @@ -64,7 +65,7 @@ internal abstract class AbstractProgressSession protected construct ) @Volatile - protected var progress = initialProgress + private var progress = initialProgress set(value) { if (field == value) { return @@ -98,6 +99,10 @@ internal abstract class AbstractProgressSession protected construct progressListeners -= listener } + protected fun setProgress(value: Int) { + progress = Progress(value, PROGRESS_MAX) + } + private fun persistSessionProgress(value: Progress) = executor.execute { sessionProgressDao.updateProgress(id.toString(), value.progress, value.max) } From bcee2d8541969eaa9a9bde6ab24911d1bb68b7ab Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Wed, 23 Oct 2024 17:16:41 +0500 Subject: [PATCH 14/40] don't hardcode session-based installer session commit progress threshold --- .../activity/SessionBasedInstallActivity.kt | 3 ++- .../session/SessionBasedInstallSession.kt | 4 +++- .../impl/session/helpers/LaunchConfirmation.kt | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/activity/SessionBasedInstallActivity.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/activity/SessionBasedInstallActivity.kt index 66473a5b3..3d97c7c3a 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/activity/SessionBasedInstallActivity.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/activity/SessionBasedInstallActivity.kt @@ -28,6 +28,7 @@ import androidx.annotation.RequiresApi import androidx.annotation.RestrictTo import ru.solrudev.ackpine.impl.installer.activity.helpers.getParcelableCompat import ru.solrudev.ackpine.impl.session.helpers.commitSession +import ru.solrudev.ackpine.impl.session.helpers.getSessionBasedSessionCommitProgressValue import ru.solrudev.ackpine.installer.InstallFailure import ru.solrudev.ackpine.session.Session @@ -85,7 +86,7 @@ internal class SessionBasedInstallConfirmationActivity : InstallActivity(CONFIRM // Hacky workaround: progress not going higher than 0.8 means session is dead. This is needed to complete // the Ackpine session with failure on reasons which are not handled in PackageInstallerStatusReceiver. // For example, "There was a problem parsing the package" error falls under that. - val isSessionAlive = sessionInfo != null && sessionInfo.progress >= 0.81 + val isSessionAlive = sessionInfo != null && sessionInfo.progress >= getSessionBasedSessionCommitProgressValue() if (!isSessionAlive) { setLoading(isLoading = true, delayMillis = 100) handler.postDelayed(deadSessionCompletionRunnable, 1000) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt index 767a00157..6fff224a5 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/session/SessionBasedInstallSession.kt @@ -48,6 +48,7 @@ import ru.solrudev.ackpine.impl.installer.session.helpers.openAssetFileDescripto import ru.solrudev.ackpine.impl.session.AbstractProgressSession import ru.solrudev.ackpine.impl.session.helpers.CANCEL_CURRENT_FLAGS import ru.solrudev.ackpine.impl.session.helpers.commitSession +import ru.solrudev.ackpine.impl.session.helpers.getSessionBasedSessionCommitProgressValue import ru.solrudev.ackpine.impl.session.helpers.launchConfirmation import ru.solrudev.ackpine.installer.InstallFailure import ru.solrudev.ackpine.installer.parameters.InstallMode @@ -107,7 +108,8 @@ internal class SessionBasedInstallSession internal constructor( if (initialState.isTerminal) { return } - if (initialProgress.progress >= 81) { // means that actual installation is ongoing or is completed + if (initialProgress.progress >= (context.getSessionBasedSessionCommitProgressValue() * PROGRESS_MAX).toInt()) { + // means that actual installation is ongoing or is completed notifyCommitted() // block clients from committing } executor.executeWithSemaphore(nativeSessionIdSemaphore) { diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/helpers/LaunchConfirmation.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/helpers/LaunchConfirmation.kt index bd6656b66..f82d0db5d 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/helpers/LaunchConfirmation.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/helpers/LaunchConfirmation.kt @@ -19,11 +19,13 @@ package ru.solrudev.ackpine.impl.session.helpers import android.app.NotificationManager import android.app.PendingIntent import android.content.Context +import android.content.Context.MODE_PRIVATE import android.content.Intent import android.content.pm.PackageInstaller import android.os.Build import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat +import androidx.core.content.edit import androidx.core.content.getSystemService import ru.solrudev.ackpine.core.R import ru.solrudev.ackpine.impl.activity.SessionCommitActivity @@ -32,6 +34,9 @@ import ru.solrudev.ackpine.session.parameters.Confirmation import ru.solrudev.ackpine.session.parameters.NotificationData import java.util.UUID +private const val ACKPINE_SESSION_BASED_INSTALLER = "ackpine_session_based_installer" +private const val SESSION_COMMIT_PROGRESS_VALUE = "session_commit_progress_value" + @get:JvmSynthetic internal val CANCEL_CURRENT_FLAGS = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_CANCEL_CURRENT @@ -87,9 +92,21 @@ internal fun PackageInstaller.commitSession( val statusReceiver = receiverPendingIntent.intentSender if (getSessionInfo(sessionId) != null) { openSession(sessionId).commit(statusReceiver) + val preferences = context.getSharedPreferences(ACKPINE_SESSION_BASED_INSTALLER, MODE_PRIVATE) + if (!preferences.contains(SESSION_COMMIT_PROGRESS_VALUE)) { + preferences.edit { + putFloat(SESSION_COMMIT_PROGRESS_VALUE, getSessionInfo(sessionId)!!.progress + 0.01f) + } + } } } +@JvmSynthetic +internal fun Context.getSessionBasedSessionCommitProgressValue(): Float { + return getSharedPreferences(ACKPINE_SESSION_BASED_INSTALLER, MODE_PRIVATE) + .getFloat(SESSION_COMMIT_PROGRESS_VALUE, 1f) +} + private fun Context.showNotification( intent: PendingIntent, notificationData: NotificationData, From 4e84827f69247f9a821e6c2f60133a7cbc59e25c Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Wed, 23 Oct 2024 17:34:06 +0500 Subject: [PATCH 15/40] disable cancel button based on session's state instead of progress in sample apps --- .../sample/install/InstallViewModel.java | 16 +++++++++---- .../sample/install/SessionDataRepository.java | 4 +++- .../install/SessionDataRepositoryImpl.java | 24 ++++++++++--------- .../sample/install/InstallViewModel.kt | 7 ++++++ .../sample/install/SessionDataRepository.kt | 3 ++- .../install/SessionDataRepositoryImpl.kt | 21 ++++++++-------- 6 files changed, 48 insertions(+), 27 deletions(-) diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java index 38731ceed..cccd980e0 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java @@ -92,8 +92,7 @@ public void installPackage(@NonNull Sequence apks, @NonNull String fileName .build()); final var sessionData = new SessionData(session.getId(), fileName); sessionDataRepository.addSessionData(sessionData); - session.addStateListener(subscriptions, new SessionStateListener(session)); - session.addProgressListener(subscriptions, sessionDataRepository::updateSessionProgress); + addSessionListeners(session); }); } @@ -153,8 +152,7 @@ private void addSessionListeners(@NonNull UUID id) { @Override public void onSuccess(@Nullable ProgressSession session) { if (session != null) { - session.addStateListener(subscriptions, new SessionStateListener(session)); - session.addProgressListener(subscriptions, sessionDataRepository::updateSessionProgress); + addSessionListeners(session); } } @@ -164,6 +162,16 @@ public void onFailure(@NonNull Throwable t) { // no-op }, MoreExecutors.directExecutor()); } + private void addSessionListeners(@NonNull ProgressSession session) { + session.addStateListener(subscriptions, new SessionStateListener(session)); + session.addProgressListener(subscriptions, sessionDataRepository::updateSessionProgress); + session.addStateListener(subscriptions, (sessionId, state) -> { + if (state instanceof Session.State.Committed) { + sessionDataRepository.updateSessionIsCancellable(sessionId, false); + } + }); + } + private List getSessionsSnapshot() { return sessionDataRepository.getSessions().getValue(); } diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepository.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepository.java index 139d11ea6..2a1d1fdb0 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepository.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepository.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Ilya Fomichev + * Copyright (C) 2023-2024 Ilya Fomichev * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,5 +39,7 @@ public interface SessionDataRepository { void updateSessionProgress(@NonNull UUID id, @NonNull Progress progress); + void updateSessionIsCancellable(@NonNull UUID id, boolean isCancellable); + void setError(@NonNull UUID id, @NonNull NotificationString error); } diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.java index 7b8b06878..437570c73 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.java @@ -87,20 +87,22 @@ public void updateSessionProgress(@NonNull UUID id, @NonNull Progress progress) sessionsProgress.set(sessionProgressIndex, new SessionProgress(id, progress)); } this.sessionsProgress.setValue(sessionsProgress); - if (progress.getProgress() <= 80) { + } + + @Override + public void updateSessionIsCancellable(@NonNull UUID id, boolean isCancellable) { + final var sessionDataIndex = getSessionDataIndexById(getCurrentSessions(), id); + if (sessionDataIndex == -1) { return; } - final var sessionDataIndex = getSessionDataIndexById(getCurrentSessions(), id); - if (sessionDataIndex != -1) { - final var sessionData = getCurrentSessions().get(sessionDataIndex); - if (!sessionData.isCancellable()) { - return; - } - final var sessions = getCurrentSessionsCopy(); - sessions.set(sessionDataIndex, - new SessionData(sessionData.id(), sessionData.name(), sessionData.error(), false)); - this.sessions.setValue(sessions); + final var sessionData = getCurrentSessions().get(sessionDataIndex); + if (sessionData.isCancellable() == isCancellable) { + return; } + final var sessions = getCurrentSessionsCopy(); + sessions.set(sessionDataIndex, + new SessionData(sessionData.id(), sessionData.name(), sessionData.error(), isCancellable)); + this.sessions.setValue(sessions); } @Override diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt index 6153b2b31..02a9ac78a 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart @@ -49,10 +50,12 @@ import ru.solrudev.ackpine.installer.createSession import ru.solrudev.ackpine.installer.getSession import ru.solrudev.ackpine.sample.R import ru.solrudev.ackpine.session.ProgressSession +import ru.solrudev.ackpine.session.Session import ru.solrudev.ackpine.session.SessionResult import ru.solrudev.ackpine.session.await import ru.solrudev.ackpine.session.parameters.NotificationString import ru.solrudev.ackpine.session.progress +import ru.solrudev.ackpine.session.state import ru.solrudev.ackpine.splits.Apk import java.util.UUID @@ -113,6 +116,10 @@ class InstallViewModel( session.progress .onEach { progress -> sessionDataRepository.updateSessionProgress(session.id, progress) } .launchIn(this) + session.state + .filterIsInstance() + .onEach { sessionDataRepository.updateSessionIsCancellable(session.id, isCancellable = false) } + .launchIn(this) try { when (val result = session.await()) { is SessionResult.Success -> sessionDataRepository.removeSessionData(session.id) diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepository.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepository.kt index b766c7cf3..1cddcc41e 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepository.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Ilya Fomichev + * Copyright (C) 2023-2024 Ilya Fomichev * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,5 +27,6 @@ interface SessionDataRepository { fun addSessionData(sessionData: SessionData) fun removeSessionData(id: UUID) fun updateSessionProgress(id: UUID, progress: Progress) + fun updateSessionIsCancellable(id: UUID, isCancellable: Boolean) fun setError(id: UUID, error: NotificationString) } \ No newline at end of file diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.kt index 60e48e30c..a09baa73d 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.kt @@ -67,19 +67,20 @@ class SessionDataRepositoryImpl(private val savedStateHandle: SavedStateHandle) sessionsProgress[sessionProgressIndex] = SessionProgress(id, progress) } _sessionsProgress = sessionsProgress - if (progress.progress <= 80) { + } + + override fun updateSessionIsCancellable(id: UUID, isCancellable: Boolean) { + val sessionDataIndex = _sessions.indexOfFirst { it.id == id } + if (sessionDataIndex == -1) { return } - val sessionDataIndex = _sessions.indexOfFirst { it.id == id } - if (sessionDataIndex != -1) { - val sessionData = _sessions[sessionDataIndex] - if (!sessionData.isCancellable) { - return - } - val sessions = _sessions.toMutableList() - sessions[sessionDataIndex] = sessionData.copy(isCancellable = false) - _sessions = sessions + val sessionData = _sessions[sessionDataIndex] + if (sessionData.isCancellable == isCancellable) { + return } + val sessions = _sessions.toMutableList() + sessions[sessionDataIndex] = sessionData.copy(isCancellable = isCancellable) + _sessions = sessions } override fun setError(id: UUID, error: NotificationString) { From 2513df2438e980bf144bfb57111b5a5276913a30 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 13:51:52 +0500 Subject: [PATCH 16/40] ensure stable notification string resources --- ackpine-core/consumer-rules.pro | 4 +- .../8.json | 586 ++++++++++++++++++ .../ackpine/impl/database/AckpineDatabase.kt | 4 +- .../database/AckpineDatabaseMigrations.kt | 90 ++- .../impl/installer/InstallSessionFactory.kt | 25 +- .../uninstaller/UninstallSessionFactory.kt | 25 +- .../session/parameters/NotificationData.kt | 12 +- .../session/parameters/NotificationString.kt | 113 ++-- docs/configuration.md | 39 +- 9 files changed, 792 insertions(+), 106 deletions(-) create mode 100644 ackpine-core/schemas/ru.solrudev.ackpine.impl.database.AckpineDatabase/8.json diff --git a/ackpine-core/consumer-rules.pro b/ackpine-core/consumer-rules.pro index f7b1e6542..ef8bb2395 100644 --- a/ackpine-core/consumer-rules.pro +++ b/ackpine-core/consumer-rules.pro @@ -1,10 +1,10 @@ # Serializable -keep class ru.solrudev.ackpine.session.parameters.NotificationString { *; } -keep class ru.solrudev.ackpine.session.parameters.NotificationString$* { *; } --keep class ru.solrudev.ackpine.session.parameters.Default { *; } +-keep class * extends ru.solrudev.ackpine.session.parameters.NotificationString$Resource +-keep class ru.solrudev.ackpine.session.parameters.DefaultNotificationString { *; } -keep class ru.solrudev.ackpine.session.parameters.Empty { *; } -keep class ru.solrudev.ackpine.session.parameters.Raw { *; } --keep class ru.solrudev.ackpine.session.parameters.Resource { *; } -keep class ru.solrudev.ackpine.installer.InstallFailure { *; } -keep class ru.solrudev.ackpine.installer.InstallFailure$* { *; } -keep class ru.solrudev.ackpine.uninstaller.UninstallFailure { *; } diff --git a/ackpine-core/schemas/ru.solrudev.ackpine.impl.database.AckpineDatabase/8.json b/ackpine-core/schemas/ru.solrudev.ackpine.impl.database.AckpineDatabase/8.json new file mode 100644 index 000000000..9dcf88230 --- /dev/null +++ b/ackpine-core/schemas/ru.solrudev.ackpine.impl.database.AckpineDatabase/8.json @@ -0,0 +1,586 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "70a447048443686563989e6b938d53cc", + "entities": [ + { + "tableName": "sessions", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT NOT NULL, `state` TEXT NOT NULL, `confirmation` TEXT NOT NULL, `notification_title` BLOB NOT NULL, `notification_text` BLOB NOT NULL, `notification_icon` INTEGER NOT NULL, `require_user_action` INTEGER NOT NULL DEFAULT true, `last_launch_timestamp` INTEGER NOT NULL DEFAULT 0, `last_commit_timestamp` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "confirmation", + "columnName": "confirmation", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationTitle", + "columnName": "notification_title", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "notificationText", + "columnName": "notification_text", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "notificationIcon", + "columnName": "notification_icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "requireUserAction", + "columnName": "require_user_action", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "true" + }, + { + "fieldPath": "lastLaunchTimestamp", + "columnName": "last_launch_timestamp", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastCommitTimestamp", + "columnName": "last_commit_timestamp", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_sessions_type", + "unique": false, + "columnNames": [ + "type" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_sessions_type` ON `${TABLE_NAME}` (`type`)" + }, + { + "name": "index_sessions_state", + "unique": false, + "columnNames": [ + "state" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_sessions_state` ON `${TABLE_NAME}` (`state`)" + }, + { + "name": "index_sessions_last_launch_timestamp", + "unique": false, + "columnNames": [ + "last_launch_timestamp" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_sessions_last_launch_timestamp` ON `${TABLE_NAME}` (`last_launch_timestamp`)" + }, + { + "name": "index_sessions_last_commit_timestamp", + "unique": false, + "columnNames": [ + "last_commit_timestamp" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_sessions_last_commit_timestamp` ON `${TABLE_NAME}` (`last_commit_timestamp`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "sessions_installer_types", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`session_id` TEXT NOT NULL, `installer_type` TEXT NOT NULL, PRIMARY KEY(`session_id`), FOREIGN KEY(`session_id`) REFERENCES `sessions`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installerType", + "columnName": "installer_type", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "session_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "sessions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "sessions_install_failures", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`session_id` TEXT NOT NULL, `failure` BLOB NOT NULL, PRIMARY KEY(`session_id`), FOREIGN KEY(`session_id`) REFERENCES `sessions`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "failure", + "columnName": "failure", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "session_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "sessions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "sessions_uninstall_failures", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`session_id` TEXT NOT NULL, `failure` BLOB NOT NULL, PRIMARY KEY(`session_id`), FOREIGN KEY(`session_id`) REFERENCES `sessions`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "failure", + "columnName": "failure", + "affinity": "BLOB", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "session_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "sessions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "sessions_install_uris", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `session_id` TEXT NOT NULL, `uri` TEXT NOT NULL, FOREIGN KEY(`session_id`) REFERENCES `sessions`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "uri", + "columnName": "uri", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_sessions_install_uris_session_id", + "unique": false, + "columnNames": [ + "session_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_sessions_install_uris_session_id` ON `${TABLE_NAME}` (`session_id`)" + } + ], + "foreignKeys": [ + { + "table": "sessions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "sessions_package_names", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `session_id` TEXT NOT NULL, `package_name` TEXT NOT NULL, FOREIGN KEY(`session_id`) REFERENCES `sessions`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "package_name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_sessions_package_names_session_id", + "unique": false, + "columnNames": [ + "session_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_sessions_package_names_session_id` ON `${TABLE_NAME}` (`session_id`)" + } + ], + "foreignKeys": [ + { + "table": "sessions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "sessions_progress", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`session_id` TEXT NOT NULL, `progress` INTEGER NOT NULL DEFAULT 0, `max` INTEGER NOT NULL DEFAULT 100, PRIMARY KEY(`session_id`), FOREIGN KEY(`session_id`) REFERENCES `sessions`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "max", + "columnName": "max", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "100" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "session_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "sessions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "sessions_native_session_ids", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`session_id` TEXT NOT NULL, `native_session_id` INTEGER NOT NULL, PRIMARY KEY(`session_id`), FOREIGN KEY(`session_id`) REFERENCES `sessions`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "nativeSessionId", + "columnName": "native_session_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "session_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "sessions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "sessions_notification_ids", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`session_id` TEXT NOT NULL, `notification_id` INTEGER NOT NULL, PRIMARY KEY(`session_id`), FOREIGN KEY(`session_id`) REFERENCES `sessions`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationId", + "columnName": "notification_id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "session_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "sessions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "sessions_names", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`session_id` TEXT NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`session_id`), FOREIGN KEY(`session_id`) REFERENCES `sessions`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "session_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "sessions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "sessions_install_modes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`session_id` TEXT NOT NULL, `install_mode` TEXT NOT NULL, PRIMARY KEY(`session_id`), FOREIGN KEY(`session_id`) REFERENCES `sessions`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "installMode", + "columnName": "install_mode", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "session_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "sessions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "sessions_last_install_timestamps", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`session_id` TEXT NOT NULL, `last_update_timestamp` INTEGER NOT NULL, PRIMARY KEY(`session_id`), FOREIGN KEY(`session_id`) REFERENCES `sessions`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "sessionId", + "columnName": "session_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastUpdateTimestamp", + "columnName": "last_update_timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "session_id" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "sessions", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "session_id" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "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, '70a447048443686563989e6b938d53cc')" + ] + } +} \ No newline at end of file diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabase.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabase.kt index 7efa1a956..66b68918e 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabase.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabase.kt @@ -79,7 +79,7 @@ private const val PURGE_SQL = "DELETE FROM sessions WHERE state IN $TERMINAL_STA AutoMigration(from = 5, to = 6), AutoMigration(from = 6, to = 7) ], - version = 7, + version = 8, exportSchema = true ) @TypeConverters( @@ -132,7 +132,7 @@ internal abstract class AckpineDatabase : RoomDatabase() { } .setQueryExecutor(executor) .addCallback(PurgeCallback) - .addMigrations(Migration_4_5) + .addMigrations(Migration_4_5, Migration_7_8) .fallbackToDestructiveMigration() .build() } diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabaseMigrations.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabaseMigrations.kt index 9d13f4142..1e8ee076c 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabaseMigrations.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabaseMigrations.kt @@ -23,40 +23,62 @@ import androidx.sqlite.db.SupportSQLiteDatabase @RestrictTo(RestrictTo.Scope.LIBRARY) internal object Migration_4_5 : Migration(4, 5) { - override fun migrate(db: SupportSQLiteDatabase) = db.run { - val supportsDeferForeignKeys = Build.VERSION.SDK_INT >= 21 - try { - if (!supportsDeferForeignKeys) { - execSQL("PRAGMA foreign_keys = FALSE") - } - beginTransaction() - if (supportsDeferForeignKeys) { - execSQL("PRAGMA defer_foreign_keys = TRUE") - } - execSQL("DROP TABLE sessions") - execSQL("CREATE TABLE IF NOT EXISTS sessions (id TEXT NOT NULL, type TEXT NOT NULL, state TEXT NOT NULL, confirmation TEXT NOT NULL, notification_title BLOB NOT NULL, notification_text BLOB NOT NULL, notification_icon INTEGER NOT NULL, require_user_action INTEGER NOT NULL DEFAULT true, last_launch_timestamp INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(id))") - execSQL("CREATE INDEX IF NOT EXISTS index_sessions_type ON sessions (type)") - execSQL("CREATE INDEX IF NOT EXISTS index_sessions_state ON sessions (state)") - execSQL("CREATE INDEX IF NOT EXISTS index_sessions_last_launch_timestamp ON sessions (last_launch_timestamp)") - execSQL("DELETE FROM sessions_installer_types") - execSQL("DELETE FROM sessions_install_failures") - execSQL("DELETE FROM sessions_uninstall_failures") - execSQL("DELETE FROM sessions_install_uris") - execSQL("DELETE FROM sessions_package_names") - execSQL("DELETE FROM sessions_progress") - execSQL("DELETE FROM sessions_native_session_ids") - execSQL("DELETE FROM sessions_notification_ids") - execSQL("DELETE FROM sessions_names") - setTransactionSuccessful() - } finally { - endTransaction() - if (!supportsDeferForeignKeys) { - execSQL("PRAGMA foreign_keys = TRUE") - } - query("PRAGMA wal_checkpoint(FULL)").close() - if (!inTransaction()) { - execSQL("VACUUM") - } + override fun migrate(db: SupportSQLiteDatabase) = db.migrate { + execSQL("DROP TABLE sessions") + execSQL("CREATE TABLE IF NOT EXISTS sessions (id TEXT NOT NULL, type TEXT NOT NULL, state TEXT NOT NULL, confirmation TEXT NOT NULL, notification_title BLOB NOT NULL, notification_text BLOB NOT NULL, notification_icon INTEGER NOT NULL, require_user_action INTEGER NOT NULL DEFAULT true, last_launch_timestamp INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(id))") + execSQL("CREATE INDEX IF NOT EXISTS index_sessions_type ON sessions (type)") + execSQL("CREATE INDEX IF NOT EXISTS index_sessions_state ON sessions (state)") + execSQL("CREATE INDEX IF NOT EXISTS index_sessions_last_launch_timestamp ON sessions (last_launch_timestamp)") + execSQL("DELETE FROM sessions_installer_types") + execSQL("DELETE FROM sessions_install_failures") + execSQL("DELETE FROM sessions_uninstall_failures") + execSQL("DELETE FROM sessions_install_uris") + execSQL("DELETE FROM sessions_package_names") + execSQL("DELETE FROM sessions_progress") + execSQL("DELETE FROM sessions_native_session_ids") + execSQL("DELETE FROM sessions_notification_ids") + execSQL("DELETE FROM sessions_names") + } +} + +@RestrictTo(RestrictTo.Scope.LIBRARY) +internal object Migration_7_8 : Migration(7, 8) { + override fun migrate(db: SupportSQLiteDatabase) = db.migrate { + execSQL("DELETE FROM sessions") + execSQL("DELETE FROM sessions_installer_types") + execSQL("DELETE FROM sessions_install_failures") + execSQL("DELETE FROM sessions_uninstall_failures") + execSQL("DELETE FROM sessions_install_uris") + execSQL("DELETE FROM sessions_package_names") + execSQL("DELETE FROM sessions_progress") + execSQL("DELETE FROM sessions_native_session_ids") + execSQL("DELETE FROM sessions_notification_ids") + execSQL("DELETE FROM sessions_names") + execSQL("DELETE FROM sessions_install_modes") + execSQL("DELETE FROM sessions_last_install_timestamps") + } +} + +private inline fun SupportSQLiteDatabase.migrate(actions: SupportSQLiteDatabase.() -> Unit) { + val supportsDeferForeignKeys = Build.VERSION.SDK_INT >= 21 + try { + if (!supportsDeferForeignKeys) { + execSQL("PRAGMA foreign_keys = FALSE") + } + beginTransaction() + if (supportsDeferForeignKeys) { + execSQL("PRAGMA defer_foreign_keys = TRUE") + } + actions() + setTransactionSuccessful() + } finally { + endTransaction() + if (!supportsDeferForeignKeys) { + execSQL("PRAGMA foreign_keys = TRUE") + } + query("PRAGMA wal_checkpoint(FULL)").close() + if (!inTransaction()) { + execSQL("VACUUM") } } } \ No newline at end of file diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt index 0236c7afb..f478d6e21 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt @@ -36,6 +36,7 @@ import ru.solrudev.ackpine.installer.parameters.InstallerType import ru.solrudev.ackpine.session.Progress import ru.solrudev.ackpine.session.ProgressSession import ru.solrudev.ackpine.session.Session +import ru.solrudev.ackpine.session.parameters.DefaultNotificationString import ru.solrudev.ackpine.session.parameters.NotificationData import ru.solrudev.ackpine.session.parameters.NotificationString import java.util.UUID @@ -108,18 +109,34 @@ internal class InstallSessionFactoryImpl internal constructor( private fun NotificationData.resolveDefault(name: String): NotificationData = NotificationData.Builder() .setTitle( - title.takeUnless { it.isDefault } ?: NotificationString.resource(R.string.ackpine_prompt_install_title) + title.takeUnless { it is DefaultNotificationString } ?: AckpinePromptInstallTitle ) .setContentText( - contentText.takeUnless { it.isDefault } ?: resolveDefaultContentText(name) + contentText.takeUnless { it is DefaultNotificationString } ?: resolveDefaultContentText(name) ) .setIcon(icon) .build() private fun resolveDefaultContentText(name: String): NotificationString { if (name.isNotEmpty()) { - return NotificationString.resource(R.string.ackpine_prompt_install_message_with_label, name) + return AckpinePromptInstallMessageWithLabel(name) } - return NotificationString.resource(R.string.ackpine_prompt_install_message) + return AckpinePromptInstallMessage + } +} + +private object AckpinePromptInstallTitle : NotificationString.Resource(R.string.ackpine_prompt_install_title) { + private const val serialVersionUID = 7815666924791958742L +} + +private object AckpinePromptInstallMessage : NotificationString.Resource(R.string.ackpine_prompt_install_message) { + private const val serialVersionUID = 1224637050663404482L +} + +private class AckpinePromptInstallMessageWithLabel(name: String) : + NotificationString.Resource(R.string.ackpine_prompt_install_message_with_label, name) { + + private companion object { + private const val serialVersionUID = -6931607904159775056L } } \ No newline at end of file diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt index bb97b27b5..ca7755e60 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt @@ -26,6 +26,7 @@ import ru.solrudev.ackpine.impl.database.dao.SessionFailureDao import ru.solrudev.ackpine.impl.uninstaller.helpers.getApplicationLabel import ru.solrudev.ackpine.impl.uninstaller.session.UninstallSession import ru.solrudev.ackpine.session.Session +import ru.solrudev.ackpine.session.parameters.DefaultNotificationString import ru.solrudev.ackpine.session.parameters.NotificationData import ru.solrudev.ackpine.session.parameters.NotificationString import ru.solrudev.ackpine.uninstaller.UninstallFailure @@ -74,10 +75,10 @@ internal class UninstallSessionFactoryImpl internal constructor( private fun NotificationData.resolveDefault(packageName: String): NotificationData = NotificationData.Builder() .setTitle( - title.takeUnless { it.isDefault } ?: NotificationString.resource(R.string.ackpine_prompt_uninstall_title) + title.takeUnless { it is DefaultNotificationString } ?: AckpinePromptUninstallTitle ) .setContentText( - contentText.takeUnless { it.isDefault } ?: resolveDefaultContentText(packageName) + contentText.takeUnless { it is DefaultNotificationString } ?: resolveDefaultContentText(packageName) ) .setIcon(icon) .build() @@ -85,8 +86,24 @@ internal class UninstallSessionFactoryImpl internal constructor( private fun resolveDefaultContentText(packageName: String): NotificationString { val label = applicationContext.packageManager.getApplicationLabel(packageName)?.toString() if (label != null) { - return NotificationString.resource(R.string.ackpine_prompt_uninstall_message_with_label, label) + return AckpinePromptUninstallMessageWithLabel(label) } - return NotificationString.resource(R.string.ackpine_prompt_uninstall_message) + return AckpinePromptUninstallMessage + } +} + +private object AckpinePromptUninstallTitle : NotificationString.Resource(R.string.ackpine_prompt_uninstall_title) { + private const val serialVersionUID = -4086992997791586590L +} + +private object AckpinePromptUninstallMessage : NotificationString.Resource(R.string.ackpine_prompt_uninstall_message) { + private const val serialVersionUID = -3150252606151986307L +} + +private class AckpinePromptUninstallMessageWithLabel(label: String) : + NotificationString.Resource(R.string.ackpine_prompt_uninstall_message_with_label, label) { + + private companion object { + private const val serialVersionUID = 5259262335605612228L } } \ No newline at end of file diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt index 413d87d2d..1813d0597 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt @@ -16,7 +16,9 @@ package ru.solrudev.ackpine.session.parameters +import android.content.Context import androidx.annotation.DrawableRes +import androidx.annotation.RestrictTo /** * Data for a high-priority notification which launches confirmation activity. @@ -73,8 +75,8 @@ public class NotificationData private constructor( @JvmField public val DEFAULT: NotificationData = NotificationData( icon = android.R.drawable.ic_dialog_alert, - title = NotificationString.default(), - contentText = NotificationString.default() + title = DefaultNotificationString, + contentText = DefaultNotificationString ) } @@ -134,4 +136,10 @@ public class NotificationData private constructor( */ public fun build(): NotificationData = NotificationData(icon, title, contentText) } +} + +@RestrictTo(RestrictTo.Scope.LIBRARY) +internal data object DefaultNotificationString : NotificationString { + private const val serialVersionUID = 809543744617543082L + override fun resolve(context: Context): String = "" } \ No newline at end of file diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationString.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationString.kt index da3d2026a..75151e281 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationString.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationString.kt @@ -23,15 +23,9 @@ import androidx.annotation.StringRes import java.io.Serializable /** - * String for a session's [confirmation notification][NotificationData]. + * String which can be resolved at use site. */ -public sealed interface NotificationString : Serializable { - - /** - * Returns whether this string represents a default string. - */ - public val isDefault: Boolean - get() = this is Default +public interface NotificationString : Serializable { /** * Returns whether this string is empty. @@ -58,12 +52,6 @@ public sealed interface NotificationString : Serializable { public companion object { - /** - * Creates a default [NotificationString]. - */ - @JvmStatic - public fun default(): NotificationString = Default - /** * Creates an empty [NotificationString]. */ @@ -82,60 +70,77 @@ public sealed interface NotificationString : Serializable { } /** - * Creates [NotificationString] represented by Android resource string with optional arguments. Arguments can be - * [NotificationStrings][NotificationString] as well. + * Creates an anonymous instance of [NotificationString.Resource], which is a [NotificationString] represented by + * Android resource string with optional arguments. Arguments can be [NotificationStrings][NotificationString] + * as well. + * + * This factory is meant to create **only** transient strings, i.e. not persisted in storage. For persisted + * strings [NotificationString.Resource] should be explicitly subclassed. Example: + * ``` + * object MessageString : NotificationString.Resource(R.string.message) + * class ErrorString(error: String) : NotificationString.Resource(R.string.error, error) + * ``` */ @JvmStatic public fun resource(@StringRes stringId: Int, vararg args: Serializable): NotificationString { - return Resource(stringId, args) + return object : Resource(stringId, args) {} } } -} -private data object Default : NotificationString { - private const val serialVersionUID = 809543744617543082L - override fun resolve(context: Context): String = "" + /** + * [NotificationString] represented by Android resource string with optional arguments. Arguments can be + * [NotificationStrings][NotificationString] as well. + * + * Should be explicitly subclassed to ensure stable persistence. Example: + * ``` + * object MessageString : NotificationString.Resource(R.string.message) + * class ErrorString(error: String) : NotificationString.Resource(R.string.error, error) + * ``` + * For transient strings, i.e. not persisted in storage, you can use [NotificationString.resource] factory. + */ + public abstract class Resource( + @[StringRes Transient] private val stringId: Int, + private vararg val args: Serializable + ) : NotificationString { + + override fun resolve(context: Context): String = context.getString(stringId, *resolveArgs(context)) + + private fun resolveArgs(context: Context): Array = args.map { argument -> + if (argument is NotificationString) { + argument.resolve(context) + } else { + argument + } + }.toTypedArray() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Resource + if (stringId != other.stringId) return false + return args.contentEquals(other.args) + } + + override fun hashCode(): Int { + var result = stringId + result = 31 * result + args.contentHashCode() + return result + } + + private companion object { + private const val serialVersionUID = -7766769726170724379L + } + } } private data object Empty : NotificationString { - private const val serialVersionUID: Long = 5194188194930148316L + private const val serialVersionUID = 5194188194930148316L override fun resolve(context: Context): String = "" } private data class Raw(val value: String) : NotificationString { override fun resolve(context: Context): String = value private companion object { - private const val serialVersionUID: Long = -6824736411987160679L - } -} - -private data class Resource(@StringRes val stringId: Int, val args: Array) : NotificationString { - - override fun resolve(context: Context): String = context.getString(stringId, *resolveArgs(context)) - - private fun resolveArgs(context: Context): Array = args.map { argument -> - if (argument is NotificationString) { - argument.resolve(context) - } else { - argument - } - }.toTypedArray() - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as Resource - if (stringId != other.stringId) return false - return args.contentEquals(other.args) - } - - override fun hashCode(): Int { - var result = stringId - result = 31 * result + args.contentHashCode() - return result - } - - private companion object { - private const val serialVersionUID: Long = -7822872422889864805L + private const val serialVersionUID = -6824736411987160679L } } \ No newline at end of file diff --git a/docs/configuration.md b/docs/configuration.md index 897b87cdd..b5ecc871f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -21,11 +21,14 @@ An example of creating a session with custom parameters: name = fileName requireUserAction = false notification { - title = NotificationString.resource(R.string.install_message_title) - contentText = NotificationString.resource(R.string.install_message, fileName) + title = InstallMessageTitle + contentText = InstallMessage(fileName) icon = R.drawable.ic_install } } + + object InstallMessageTitle : NotificationString.Resource(R.string.install_message_title) + class InstallMessage(fileName: String) : NotificationString.Resource(R.string.install_message, fileName) ``` === "Java" @@ -39,11 +42,39 @@ An example of creating a session with custom parameters: .setName(fileName) .setRequireUserAction(false) .setNotificationData(new NotificationData.Builder() - .setTitle(NotificationString.resource(R.string.install_message_title)) - .setContentText(NotificationString.resource(R.string.install_message, fileName)) + .setTitle(Resources.INSTALL_MESSAGE_TITLE) + .setContentText(new Resources.InstallMessage(fileName)) .setIcon(R.drawable.ic_install) .build()) .build()); + + public class Resources { + + public static final NotificationString INSTALL_MESSAGE_TITLE = new InstallMessageTitle(); + + private static class InstallMessageTitle extends NotificationString.Resource { + + @Serial + private static final long serialVersionUID = -1310602635578779088L; + + public InstallMessageTitle() { + super(R.string.install_message_title); + } + } + + public static class InstallMessage extends NotificationString.Resource { + + @Serial + private static final long serialVersionUID = 4749568844072243110L; + + public InstallMessage(String fileName) { + super(R.string.install_message, fileName); + } + } + + private Resources() { + } + } ``` User's confirmation From 1782bb1c06605dc0f6407d9df2a10146d9c9a3b4 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 14:06:39 +0500 Subject: [PATCH 17/40] rename NotificationString to ResolvableString --- ackpine-core/consumer-rules.pro | 6 +-- .../impl/database/converters/Converters.kt | 6 +-- .../impl/database/model/SessionEntity.kt | 6 +-- .../impl/installer/InstallSessionFactory.kt | 10 ++--- .../uninstaller/UninstallSessionFactory.kt | 10 ++--- .../session/parameters/NotificationData.kt | 14 +++---- ...ificationString.kt => ResolvableString.kt} | 40 +++++++++---------- .../session/parameters/NotificationDataDsl.kt | 8 ++-- docs/configuration.md | 19 ++++++--- .../install/InstallSessionsAdapter.java | 4 +- .../sample/install/InstallViewModel.java | 24 +++++------ .../ackpine/sample/install/SessionData.java | 6 +-- .../sample/install/SessionDataRepository.java | 4 +- .../install/SessionDataRepositoryImpl.java | 4 +- .../sample/install/InstallSessionsAdapter.kt | 4 +- .../ackpine/sample/install/InstallUiState.kt | 6 +-- .../sample/install/InstallViewModel.kt | 22 +++++----- .../ackpine/sample/install/SessionData.kt | 4 +- .../sample/install/SessionDataRepository.kt | 4 +- .../install/SessionDataRepositoryImpl.kt | 4 +- 20 files changed, 106 insertions(+), 99 deletions(-) rename ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/{NotificationString.kt => ResolvableString.kt} (71%) diff --git a/ackpine-core/consumer-rules.pro b/ackpine-core/consumer-rules.pro index ef8bb2395..a796ec204 100644 --- a/ackpine-core/consumer-rules.pro +++ b/ackpine-core/consumer-rules.pro @@ -1,7 +1,7 @@ # Serializable --keep class ru.solrudev.ackpine.session.parameters.NotificationString { *; } --keep class ru.solrudev.ackpine.session.parameters.NotificationString$* { *; } --keep class * extends ru.solrudev.ackpine.session.parameters.NotificationString$Resource +-keep class ru.solrudev.ackpine.session.parameters.ResolvableString { *; } +-keep class ru.solrudev.ackpine.session.parameters.ResolvableString$* { *; } +-keep class * extends ru.solrudev.ackpine.session.parameters.ResolvableString$Resource -keep class ru.solrudev.ackpine.session.parameters.DefaultNotificationString { *; } -keep class ru.solrudev.ackpine.session.parameters.Empty { *; } -keep class ru.solrudev.ackpine.session.parameters.Raw { *; } diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/converters/Converters.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/converters/Converters.kt index d6137c178..8a38807d1 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/converters/Converters.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/converters/Converters.kt @@ -18,18 +18,18 @@ package ru.solrudev.ackpine.impl.database.converters import androidx.room.TypeConverter import ru.solrudev.ackpine.installer.InstallFailure -import ru.solrudev.ackpine.session.parameters.NotificationString +import ru.solrudev.ackpine.session.parameters.ResolvableString import ru.solrudev.ackpine.uninstaller.UninstallFailure internal object NotificationStringConverters { @TypeConverter @JvmStatic - internal fun fromByteArray(byteArray: ByteArray): NotificationString = byteArray.deserialize() + internal fun fromByteArray(byteArray: ByteArray): ResolvableString = byteArray.deserialize() @TypeConverter @JvmStatic - internal fun toByteArray(notificationString: NotificationString): ByteArray = notificationString.serialize() + internal fun toByteArray(resolvableString: ResolvableString): ByteArray = resolvableString.serialize() } internal object InstallFailureConverters { diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/model/SessionEntity.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/model/SessionEntity.kt index 739944937..e4ecd31c8 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/model/SessionEntity.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/model/SessionEntity.kt @@ -25,7 +25,7 @@ import androidx.room.PrimaryKey import androidx.room.Relation import ru.solrudev.ackpine.installer.parameters.InstallerType import ru.solrudev.ackpine.session.parameters.Confirmation -import ru.solrudev.ackpine.session.parameters.NotificationString +import ru.solrudev.ackpine.session.parameters.ResolvableString @RestrictTo(RestrictTo.Scope.LIBRARY) @Entity(tableName = "sessions") @@ -40,9 +40,9 @@ internal data class SessionEntity internal constructor( @ColumnInfo(name = "confirmation") val confirmation: Confirmation, @ColumnInfo(name = "notification_title") - val notificationTitle: NotificationString, + val notificationTitle: ResolvableString, @ColumnInfo(name = "notification_text") - val notificationText: NotificationString, + val notificationText: ResolvableString, @DrawableRes @ColumnInfo(name = "notification_icon") val notificationIcon: Int, diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt index f478d6e21..aebae5d26 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt @@ -38,7 +38,7 @@ import ru.solrudev.ackpine.session.ProgressSession import ru.solrudev.ackpine.session.Session import ru.solrudev.ackpine.session.parameters.DefaultNotificationString import ru.solrudev.ackpine.session.parameters.NotificationData -import ru.solrudev.ackpine.session.parameters.NotificationString +import ru.solrudev.ackpine.session.parameters.ResolvableString import java.util.UUID import java.util.concurrent.Executor @@ -117,7 +117,7 @@ internal class InstallSessionFactoryImpl internal constructor( .setIcon(icon) .build() - private fun resolveDefaultContentText(name: String): NotificationString { + private fun resolveDefaultContentText(name: String): ResolvableString { if (name.isNotEmpty()) { return AckpinePromptInstallMessageWithLabel(name) } @@ -125,16 +125,16 @@ internal class InstallSessionFactoryImpl internal constructor( } } -private object AckpinePromptInstallTitle : NotificationString.Resource(R.string.ackpine_prompt_install_title) { +private object AckpinePromptInstallTitle : ResolvableString.Resource(R.string.ackpine_prompt_install_title) { private const val serialVersionUID = 7815666924791958742L } -private object AckpinePromptInstallMessage : NotificationString.Resource(R.string.ackpine_prompt_install_message) { +private object AckpinePromptInstallMessage : ResolvableString.Resource(R.string.ackpine_prompt_install_message) { private const val serialVersionUID = 1224637050663404482L } private class AckpinePromptInstallMessageWithLabel(name: String) : - NotificationString.Resource(R.string.ackpine_prompt_install_message_with_label, name) { + ResolvableString.Resource(R.string.ackpine_prompt_install_message_with_label, name) { private companion object { private const val serialVersionUID = -6931607904159775056L diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt index ca7755e60..00d84b4b2 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt @@ -28,7 +28,7 @@ import ru.solrudev.ackpine.impl.uninstaller.session.UninstallSession import ru.solrudev.ackpine.session.Session import ru.solrudev.ackpine.session.parameters.DefaultNotificationString import ru.solrudev.ackpine.session.parameters.NotificationData -import ru.solrudev.ackpine.session.parameters.NotificationString +import ru.solrudev.ackpine.session.parameters.ResolvableString import ru.solrudev.ackpine.uninstaller.UninstallFailure import ru.solrudev.ackpine.uninstaller.parameters.UninstallParameters import java.util.UUID @@ -83,7 +83,7 @@ internal class UninstallSessionFactoryImpl internal constructor( .setIcon(icon) .build() - private fun resolveDefaultContentText(packageName: String): NotificationString { + private fun resolveDefaultContentText(packageName: String): ResolvableString { val label = applicationContext.packageManager.getApplicationLabel(packageName)?.toString() if (label != null) { return AckpinePromptUninstallMessageWithLabel(label) @@ -92,16 +92,16 @@ internal class UninstallSessionFactoryImpl internal constructor( } } -private object AckpinePromptUninstallTitle : NotificationString.Resource(R.string.ackpine_prompt_uninstall_title) { +private object AckpinePromptUninstallTitle : ResolvableString.Resource(R.string.ackpine_prompt_uninstall_title) { private const val serialVersionUID = -4086992997791586590L } -private object AckpinePromptUninstallMessage : NotificationString.Resource(R.string.ackpine_prompt_uninstall_message) { +private object AckpinePromptUninstallMessage : ResolvableString.Resource(R.string.ackpine_prompt_uninstall_message) { private const val serialVersionUID = -3150252606151986307L } private class AckpinePromptUninstallMessageWithLabel(label: String) : - NotificationString.Resource(R.string.ackpine_prompt_uninstall_message_with_label, label) { + ResolvableString.Resource(R.string.ackpine_prompt_uninstall_message_with_label, label) { private companion object { private const val serialVersionUID = 5259262335605612228L diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt index 1813d0597..ac591822b 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt @@ -35,12 +35,12 @@ public class NotificationData private constructor( /** * Notification title. */ - public val title: NotificationString, + public val title: ResolvableString, /** * Notification text. */ - public val contentText: NotificationString + public val contentText: ResolvableString ) { override fun toString(): String { @@ -99,7 +99,7 @@ public class NotificationData private constructor( * * By default, a string from Ackpine library is used. */ - public var title: NotificationString = DEFAULT.title + public var title: ResolvableString = DEFAULT.title private set /** @@ -107,7 +107,7 @@ public class NotificationData private constructor( * * By default, a string from Ackpine library is used. */ - public var contentText: NotificationString = DEFAULT.contentText + public var contentText: ResolvableString = DEFAULT.contentText private set /** @@ -120,14 +120,14 @@ public class NotificationData private constructor( /** * Sets [NotificationData.title]. */ - public fun setTitle(title: NotificationString): Builder = apply { + public fun setTitle(title: ResolvableString): Builder = apply { this.title = title } /** * Sets [NotificationData.contentText]. */ - public fun setContentText(contentText: NotificationString): Builder = apply { + public fun setContentText(contentText: ResolvableString): Builder = apply { this.contentText = contentText } @@ -139,7 +139,7 @@ public class NotificationData private constructor( } @RestrictTo(RestrictTo.Scope.LIBRARY) -internal data object DefaultNotificationString : NotificationString { +internal data object DefaultNotificationString : ResolvableString { private const val serialVersionUID = 809543744617543082L override fun resolve(context: Context): String = "" } \ No newline at end of file diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationString.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt similarity index 71% rename from ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationString.kt rename to ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt index 75151e281..a8ecc0078 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationString.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt @@ -25,7 +25,7 @@ import java.io.Serializable /** * String which can be resolved at use site. */ -public interface NotificationString : Serializable { +public interface ResolvableString : Serializable { /** * Returns whether this string is empty. @@ -53,16 +53,16 @@ public interface NotificationString : Serializable { public companion object { /** - * Creates an empty [NotificationString]. + * Creates an empty [ResolvableString]. */ @JvmStatic - public fun empty(): NotificationString = Empty + public fun empty(): ResolvableString = Empty /** - * Creates [NotificationString] with a hardcoded value. + * Creates [ResolvableString] with a hardcoded value. */ @JvmStatic - public fun raw(value: String): NotificationString { + public fun raw(value: String): ResolvableString { if (value.isEmpty()) { return Empty } @@ -70,43 +70,43 @@ public interface NotificationString : Serializable { } /** - * Creates an anonymous instance of [NotificationString.Resource], which is a [NotificationString] represented by - * Android resource string with optional arguments. Arguments can be [NotificationStrings][NotificationString] + * Creates an anonymous instance of [ResolvableString.Resource], which is a [ResolvableString] represented by + * Android resource string with optional arguments. Arguments can be [ResolvableStrings][ResolvableString] * as well. * * This factory is meant to create **only** transient strings, i.e. not persisted in storage. For persisted - * strings [NotificationString.Resource] should be explicitly subclassed. Example: + * strings [ResolvableString.Resource] should be explicitly subclassed. Example: * ``` - * object MessageString : NotificationString.Resource(R.string.message) - * class ErrorString(error: String) : NotificationString.Resource(R.string.error, error) + * object MessageString : ResolvableString.Resource(R.string.message) + * class ErrorString(error: String) : ResolvableString.Resource(R.string.error, error) * ``` */ @JvmStatic - public fun resource(@StringRes stringId: Int, vararg args: Serializable): NotificationString { + public fun resource(@StringRes stringId: Int, vararg args: Serializable): ResolvableString { return object : Resource(stringId, args) {} } } /** - * [NotificationString] represented by Android resource string with optional arguments. Arguments can be - * [NotificationStrings][NotificationString] as well. + * [ResolvableString] represented by Android resource string with optional arguments. Arguments can be + * [ResolvableStrings][ResolvableString] as well. * * Should be explicitly subclassed to ensure stable persistence. Example: * ``` - * object MessageString : NotificationString.Resource(R.string.message) - * class ErrorString(error: String) : NotificationString.Resource(R.string.error, error) + * object MessageString : ResolvableString.Resource(R.string.message) + * class ErrorString(error: String) : ResolvableString.Resource(R.string.error, error) * ``` - * For transient strings, i.e. not persisted in storage, you can use [NotificationString.resource] factory. + * For transient strings, i.e. not persisted in storage, you can use [ResolvableString.resource] factory. */ public abstract class Resource( @[StringRes Transient] private val stringId: Int, private vararg val args: Serializable - ) : NotificationString { + ) : ResolvableString { override fun resolve(context: Context): String = context.getString(stringId, *resolveArgs(context)) private fun resolveArgs(context: Context): Array = args.map { argument -> - if (argument is NotificationString) { + if (argument is ResolvableString) { argument.resolve(context) } else { argument @@ -133,12 +133,12 @@ public interface NotificationString : Serializable { } } -private data object Empty : NotificationString { +private data object Empty : ResolvableString { private const val serialVersionUID = 5194188194930148316L override fun resolve(context: Context): String = "" } -private data class Raw(val value: String) : NotificationString { +private data class Raw(val value: String) : ResolvableString { override fun resolve(context: Context): String = value private companion object { private const val serialVersionUID = -6824736411987160679L diff --git a/ackpine-ktx/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationDataDsl.kt b/ackpine-ktx/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationDataDsl.kt index 969894995..4a2d6daa3 100644 --- a/ackpine-ktx/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationDataDsl.kt +++ b/ackpine-ktx/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationDataDsl.kt @@ -40,14 +40,14 @@ public interface NotificationDataDsl { * * By default, a string from Ackpine library is used. */ - public var title: NotificationString + public var title: ResolvableString /** * Notification text. * * By default, a string from Ackpine library is used. */ - public var contentText: NotificationString + public var contentText: ResolvableString } @PublishedApi @@ -61,13 +61,13 @@ internal class NotificationDataDslBuilder : NotificationDataDsl { builder.setIcon(value) } - override var title: NotificationString + override var title: ResolvableString get() = builder.title set(value) { builder.setTitle(value) } - override var contentText: NotificationString + override var contentText: ResolvableString get() = builder.contentText set(value) { builder.setContentText(value) diff --git a/docs/configuration.md b/docs/configuration.md index b5ecc871f..561f6e4db 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -27,8 +27,15 @@ An example of creating a session with custom parameters: } } - object InstallMessageTitle : NotificationString.Resource(R.string.install_message_title) - class InstallMessage(fileName: String) : NotificationString.Resource(R.string.install_message, fileName) + object InstallMessageTitle : ResolvableString.Resource(R.string.install_message_title) { + private const val serialVersionUID = -1310602635578779088L + } + + class InstallMessage(fileName: String) : ResolvableString.Resource(R.string.install_message, fileName) { + private companion object { + private const val serialVersionUID = 4749568844072243110L + } + } ``` === "Java" @@ -50,9 +57,9 @@ An example of creating a session with custom parameters: public class Resources { - public static final NotificationString INSTALL_MESSAGE_TITLE = new InstallMessageTitle(); + public static final ResolvableString INSTALL_MESSAGE_TITLE = new InstallMessageTitle(); - private static class InstallMessageTitle extends NotificationString.Resource { + private static class InstallMessageTitle extends ResolvableString.Resource { @Serial private static final long serialVersionUID = -1310602635578779088L; @@ -62,7 +69,7 @@ An example of creating a session with custom parameters: } } - public static class InstallMessage extends NotificationString.Resource { + public static class InstallMessage extends ResolvableString.Resource { @Serial private static final long serialVersionUID = 4749568844072243110L; @@ -110,7 +117,7 @@ It is possible to provide notification title, text and icon. !!! Note Any configuration for notification will be ignored if `Confirmation` is set to `IMMEDIATE`, because the notification will not be shown. -`NotificationString` is a type used for `NotificationData` text values. It allows to incapsulate an Android string resource (with arguments) which will be resolved only when notification will be shown, a hardcoded string value or a default value from Ackpine library. +`ResolvableString` is a type used for `NotificationData` text values. It allows to incapsulate an Android string resource (with arguments) which will be resolved only when notification will be shown, a hardcoded string value or a default value from Ackpine library if nothing was set. `android.R.drawable.ic_dialog_alert` is used as a default icon. diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java index a6f168c53..d9f91cc4d 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java @@ -41,7 +41,7 @@ import ru.solrudev.ackpine.sample.R; import ru.solrudev.ackpine.sample.databinding.ItemInstallSessionBinding; import ru.solrudev.ackpine.session.Progress; -import ru.solrudev.ackpine.session.parameters.NotificationString; +import ru.solrudev.ackpine.session.parameters.ResolvableString; public final class InstallSessionsAdapter extends ListAdapter { @@ -109,7 +109,7 @@ public void setProgress(@NonNull Progress sessionProgress, boolean animate) { R.string.percentage, (int) (((double) progress) / max * 100))); } - private void setError(@NonNull NotificationString error) { + private void setError(@NonNull ResolvableString error) { final var fade = new Fade(); fade.setDuration(150); TransitionManager.beginDelayedTransition(binding.getRoot(), fade); diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java index cccd980e0..a95cacd4e 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java @@ -55,12 +55,12 @@ import ru.solrudev.ackpine.session.Failure; import ru.solrudev.ackpine.session.ProgressSession; import ru.solrudev.ackpine.session.Session; -import ru.solrudev.ackpine.session.parameters.NotificationString; +import ru.solrudev.ackpine.session.parameters.ResolvableString; import ru.solrudev.ackpine.splits.Apk; public final class InstallViewModel extends ViewModel { - private final MutableLiveData error = new MutableLiveData<>(); + private final MutableLiveData error = new MutableLiveData<>(); private final DisposableSubscriptionContainer subscriptions = new DisposableSubscriptionContainer(); private final PackageInstaller packageInstaller; private final SessionDataRepository sessionDataRepository; @@ -97,7 +97,7 @@ public void installPackage(@NonNull Sequence apks, @NonNull String fileName } @NonNull - public LiveData getError() { + public LiveData getError() { return error; } @@ -112,7 +112,7 @@ public LiveData> getSessionsProgress() { } public void clearError() { - error.setValue(NotificationString.empty()); + error.setValue(ResolvableString.empty()); } public void cancelSession(@NonNull UUID id) { @@ -187,22 +187,22 @@ private List mapApkSequenceToUri(@NonNull Sequence apks) { return uris; } catch (SplitPackageException exception) { if (exception instanceof NoBaseApkException) { - error.postValue(NotificationString.resource(R.string.error_no_base_apk)); + error.postValue(ResolvableString.resource(R.string.error_no_base_apk)); } else if (exception instanceof ConflictingBaseApkException) { - error.postValue(NotificationString.resource(R.string.error_conflicting_base_apk)); + error.postValue(ResolvableString.resource(R.string.error_conflicting_base_apk)); } else if (exception instanceof ConflictingSplitNameException e) { - error.postValue(NotificationString.resource(R.string.error_conflicting_split_name, e.getName())); + error.postValue(ResolvableString.resource(R.string.error_conflicting_split_name, e.getName())); } else if (exception instanceof ConflictingPackageNameException e) { - error.postValue(NotificationString.resource(R.string.error_conflicting_package_name, + error.postValue(ResolvableString.resource(R.string.error_conflicting_package_name, e.getExpected(), e.getActual(), e.getName())); } else if (exception instanceof ConflictingVersionCodeException e) { - error.postValue(NotificationString.resource(R.string.error_conflicting_version_code, + error.postValue(ResolvableString.resource(R.string.error_conflicting_version_code, e.getExpected(), e.getActual(), e.getName())); } return Collections.emptyList(); } catch (Exception exception) { final var message = exception.getMessage() != null ? exception.getMessage() : ""; - error.postValue(NotificationString.raw(message)); + error.postValue(ResolvableString.raw(message)); Log.e("InstallViewModel", null, exception); return Collections.emptyList(); } @@ -228,8 +228,8 @@ public void onSuccess(@NonNull UUID sessionId) { public void onFailure(@NonNull UUID sessionId, @NonNull InstallFailure failure) { final var message = failure.getMessage(); final var error = message != null - ? NotificationString.resource(R.string.session_error_with_reason, message) - : NotificationString.resource(R.string.session_error); + ? ResolvableString.resource(R.string.session_error_with_reason, message) + : ResolvableString.resource(R.string.session_error); sessionDataRepository.setError(sessionId, error); if (failure instanceof Failure.Exceptional f) { Log.e("InstallViewModel", null, f.getException()); diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionData.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionData.java index f10934d84..1b2e9950a 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionData.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionData.java @@ -22,17 +22,17 @@ import java.io.Serializable; import java.util.UUID; -import ru.solrudev.ackpine.session.parameters.NotificationString; +import ru.solrudev.ackpine.session.parameters.ResolvableString; public record SessionData(@NonNull UUID id, @NonNull String name, - @NonNull NotificationString error, + @NonNull ResolvableString error, boolean isCancellable) implements Serializable { @Serial private static final long serialVersionUID = -7412725679599146483L; public SessionData(@NonNull UUID id, @NonNull String name) { - this(id, name, NotificationString.empty(), true); + this(id, name, ResolvableString.empty(), true); } } \ No newline at end of file diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepository.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepository.java index 2a1d1fdb0..5955fbc8d 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepository.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepository.java @@ -23,7 +23,7 @@ import java.util.UUID; import ru.solrudev.ackpine.session.Progress; -import ru.solrudev.ackpine.session.parameters.NotificationString; +import ru.solrudev.ackpine.session.parameters.ResolvableString; public interface SessionDataRepository { @@ -41,5 +41,5 @@ public interface SessionDataRepository { void updateSessionIsCancellable(@NonNull UUID id, boolean isCancellable); - void setError(@NonNull UUID id, @NonNull NotificationString error); + void setError(@NonNull UUID id, @NonNull ResolvableString error); } diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.java index 437570c73..5d06fdd71 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.java @@ -27,7 +27,7 @@ import java.util.UUID; import ru.solrudev.ackpine.session.Progress; -import ru.solrudev.ackpine.session.parameters.NotificationString; +import ru.solrudev.ackpine.session.parameters.ResolvableString; public final class SessionDataRepositoryImpl implements SessionDataRepository { @@ -106,7 +106,7 @@ public void updateSessionIsCancellable(@NonNull UUID id, boolean isCancellable) } @Override - public void setError(@NonNull UUID id, @NonNull NotificationString error) { + public void setError(@NonNull UUID id, @NonNull ResolvableString error) { final var sessions = getCurrentSessionsCopy(); final var sessionDataIndex = getSessionDataIndexById(sessions, id); if (sessionDataIndex != -1) { diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.kt index 415ec4626..2cc6ce5cc 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.kt @@ -31,7 +31,7 @@ import androidx.transition.TransitionManager import ru.solrudev.ackpine.sample.R import ru.solrudev.ackpine.sample.databinding.ItemInstallSessionBinding import ru.solrudev.ackpine.session.Progress -import ru.solrudev.ackpine.session.parameters.NotificationString +import ru.solrudev.ackpine.session.parameters.ResolvableString import java.util.UUID class InstallSessionsAdapter( @@ -87,7 +87,7 @@ class InstallSessionsAdapter( ) } - private fun setError(error: NotificationString) = with(itemBinding) { + private fun setError(error: ResolvableString) = with(itemBinding) { TransitionManager.beginDelayedTransition(root, Fade().apply { duration = 150 }) val hasError = !error.isEmpty textViewSessionName.isVisible = !hasError diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallUiState.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallUiState.kt index 223123b6c..ed38d33a8 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallUiState.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallUiState.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Ilya Fomichev + * Copyright (C) 2023-2024 Ilya Fomichev * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,10 +16,10 @@ package ru.solrudev.ackpine.sample.install -import ru.solrudev.ackpine.session.parameters.NotificationString +import ru.solrudev.ackpine.session.parameters.ResolvableString data class InstallUiState( - val error: NotificationString = NotificationString.empty(), + val error: ResolvableString = ResolvableString.empty(), val sessions: List = emptyList(), val sessionsProgress: List = emptyList() ) \ No newline at end of file diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt index 02a9ac78a..f5f51c74a 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt @@ -53,7 +53,7 @@ import ru.solrudev.ackpine.session.ProgressSession import ru.solrudev.ackpine.session.Session import ru.solrudev.ackpine.session.SessionResult import ru.solrudev.ackpine.session.await -import ru.solrudev.ackpine.session.parameters.NotificationString +import ru.solrudev.ackpine.session.parameters.ResolvableString import ru.solrudev.ackpine.session.progress import ru.solrudev.ackpine.session.state import ru.solrudev.ackpine.splits.Apk @@ -64,7 +64,7 @@ class InstallViewModel( private val sessionDataRepository: SessionDataRepository ) : ViewModel() { - private val error = MutableStateFlow(NotificationString.empty()) + private val error = MutableStateFlow(ResolvableString.empty()) val uiState = combine( error, @@ -96,7 +96,7 @@ class InstallViewModel( fun removeSession(id: UUID) = sessionDataRepository.removeSessionData(id) fun clearError() { - error.value = NotificationString.empty() + error.value = ResolvableString.empty() } private fun awaitSessionsFromSavedState() = viewModelScope.launch { @@ -136,9 +136,9 @@ class InstallViewModel( private fun handleSessionError(message: String?, sessionId: UUID) { val error = if (message != null) { - NotificationString.resource(R.string.session_error_with_reason, message) + ResolvableString.resource(R.string.session_error_with_reason, message) } else { - NotificationString.resource(R.string.session_error) + ResolvableString.resource(R.string.session_error) } sessionDataRepository.setError(sessionId, error) } @@ -148,19 +148,19 @@ class InstallViewModel( return map { it.uri }.toList() } catch (exception: SplitPackageException) { val errorString = when (exception) { - is NoBaseApkException -> NotificationString.resource(R.string.error_no_base_apk) - is ConflictingBaseApkException -> NotificationString.resource(R.string.error_conflicting_base_apk) - is ConflictingSplitNameException -> NotificationString.resource( + is NoBaseApkException -> ResolvableString.resource(R.string.error_no_base_apk) + is ConflictingBaseApkException -> ResolvableString.resource(R.string.error_conflicting_base_apk) + is ConflictingSplitNameException -> ResolvableString.resource( R.string.error_conflicting_split_name, exception.name ) - is ConflictingPackageNameException -> NotificationString.resource( + is ConflictingPackageNameException -> ResolvableString.resource( R.string.error_conflicting_package_name, exception.expected, exception.actual, exception.name ) - is ConflictingVersionCodeException -> NotificationString.resource( + is ConflictingVersionCodeException -> ResolvableString.resource( R.string.error_conflicting_version_code, exception.expected, exception.actual, exception.name ) @@ -168,7 +168,7 @@ class InstallViewModel( error.value = errorString return emptyList() } catch (exception: Exception) { - error.value = NotificationString.raw(exception.message.orEmpty()) + error.value = ResolvableString.raw(exception.message.orEmpty()) Log.e("InstallViewModel", null, exception) return emptyList() } diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionData.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionData.kt index af98b159d..b90a69ee1 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionData.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionData.kt @@ -16,14 +16,14 @@ package ru.solrudev.ackpine.sample.install -import ru.solrudev.ackpine.session.parameters.NotificationString +import ru.solrudev.ackpine.session.parameters.ResolvableString import java.io.Serializable import java.util.UUID data class SessionData( val id: UUID, val name: String, - val error: NotificationString = NotificationString.empty(), + val error: ResolvableString = ResolvableString.empty(), val isCancellable: Boolean = true ) : Serializable { private companion object { diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepository.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepository.kt index 1cddcc41e..797cc2eb3 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepository.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepository.kt @@ -18,7 +18,7 @@ package ru.solrudev.ackpine.sample.install import kotlinx.coroutines.flow.StateFlow import ru.solrudev.ackpine.session.Progress -import ru.solrudev.ackpine.session.parameters.NotificationString +import ru.solrudev.ackpine.session.parameters.ResolvableString import java.util.UUID interface SessionDataRepository { @@ -28,5 +28,5 @@ interface SessionDataRepository { fun removeSessionData(id: UUID) fun updateSessionProgress(id: UUID, progress: Progress) fun updateSessionIsCancellable(id: UUID, isCancellable: Boolean) - fun setError(id: UUID, error: NotificationString) + fun setError(id: UUID, error: ResolvableString) } \ No newline at end of file diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.kt index a09baa73d..c5bb7d02d 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.kt @@ -18,7 +18,7 @@ package ru.solrudev.ackpine.sample.install import androidx.lifecycle.SavedStateHandle import ru.solrudev.ackpine.session.Progress -import ru.solrudev.ackpine.session.parameters.NotificationString +import ru.solrudev.ackpine.session.parameters.ResolvableString import java.util.UUID private const val SESSIONS_KEY = "SESSIONS" @@ -83,7 +83,7 @@ class SessionDataRepositoryImpl(private val savedStateHandle: SavedStateHandle) _sessions = sessions } - override fun setError(id: UUID, error: NotificationString) { + override fun setError(id: UUID, error: ResolvableString) { val sessions = _sessions.toMutableList() val sessionDataIndex = sessions.indexOfFirst { it.id == id } if (sessionDataIndex != -1) { From d679961330f45aabec9d6ffe7c947d678f66dbf4 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 14:13:49 +0500 Subject: [PATCH 18/40] improve ResolvableString.Resource example --- .../session/parameters/ResolvableString.kt | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt index a8ecc0078..d7994309d 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt @@ -77,10 +77,18 @@ public interface ResolvableString : Serializable { * This factory is meant to create **only** transient strings, i.e. not persisted in storage. For persisted * strings [ResolvableString.Resource] should be explicitly subclassed. Example: * ``` - * object MessageString : ResolvableString.Resource(R.string.message) - * class ErrorString(error: String) : ResolvableString.Resource(R.string.error, error) + * object InstallMessageTitle : ResolvableString.Resource(R.string.install_message_title) { + * private const val serialVersionUID = -1310602635578779088L + * } + * + * class InstallMessage(fileName: String) : ResolvableString.Resource(R.string.install_message, fileName) { + * private companion object { + * private const val serialVersionUID = 4749568844072243110L + * } + * } * ``` */ + @Suppress("serial") @JvmStatic public fun resource(@StringRes stringId: Int, vararg args: Serializable): ResolvableString { return object : Resource(stringId, args) {} @@ -93,8 +101,15 @@ public interface ResolvableString : Serializable { * * Should be explicitly subclassed to ensure stable persistence. Example: * ``` - * object MessageString : ResolvableString.Resource(R.string.message) - * class ErrorString(error: String) : ResolvableString.Resource(R.string.error, error) + * object InstallMessageTitle : ResolvableString.Resource(R.string.install_message_title) { + * private const val serialVersionUID = -1310602635578779088L + * } + * + * class InstallMessage(fileName: String) : ResolvableString.Resource(R.string.install_message, fileName) { + * private companion object { + * private const val serialVersionUID = 4749568844072243110L + * } + * } * ``` * For transient strings, i.e. not persisted in storage, you can use [ResolvableString.resource] factory. */ From d4f7860c638a4d7cca301c2eca8662403428ae11 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 14:19:34 +0500 Subject: [PATCH 19/40] make ResolvableString sealed --- ackpine-core/consumer-rules.pro | 1 - .../ru/solrudev/ackpine/session/parameters/ResolvableString.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/ackpine-core/consumer-rules.pro b/ackpine-core/consumer-rules.pro index a796ec204..71acb4365 100644 --- a/ackpine-core/consumer-rules.pro +++ b/ackpine-core/consumer-rules.pro @@ -2,7 +2,6 @@ -keep class ru.solrudev.ackpine.session.parameters.ResolvableString { *; } -keep class ru.solrudev.ackpine.session.parameters.ResolvableString$* { *; } -keep class * extends ru.solrudev.ackpine.session.parameters.ResolvableString$Resource --keep class ru.solrudev.ackpine.session.parameters.DefaultNotificationString { *; } -keep class ru.solrudev.ackpine.session.parameters.Empty { *; } -keep class ru.solrudev.ackpine.session.parameters.Raw { *; } -keep class ru.solrudev.ackpine.installer.InstallFailure { *; } diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt index d7994309d..d5b652495 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt @@ -25,7 +25,7 @@ import java.io.Serializable /** * String which can be resolved at use site. */ -public interface ResolvableString : Serializable { +public sealed interface ResolvableString : Serializable { /** * Returns whether this string is empty. From c17eac536e743f9da40084dc0d5f29adf9994677 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 14:35:43 +0500 Subject: [PATCH 20/40] extract ResolvableString into ackpine-resources module --- ackpine-core/build.gradle.kts | 3 +- ackpine-core/consumer-rules.pro | 5 --- .../impl/installer/InstallSessionFactory.kt | 6 ++-- .../uninstaller/UninstallSessionFactory.kt | 6 ++-- .../session/parameters/NotificationData.kt | 11 +++---- ackpine-resources/build.gradle.kts | 33 +++++++++++++++++++ ackpine-resources/consumer-rules.pro | 6 ++++ ackpine-resources/proguard-rules.pro | 21 ++++++++++++ .../src/main/AndroidManifest.xml | 18 ++++++++++ .../session/parameters/ResolvableString.kt | 0 sample-java/build.gradle.kts | 1 + sample-ktx/build.gradle.kts | 1 + settings.gradle.kts | 1 + 13 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 ackpine-resources/build.gradle.kts create mode 100644 ackpine-resources/consumer-rules.pro create mode 100644 ackpine-resources/proguard-rules.pro create mode 100644 ackpine-resources/src/main/AndroidManifest.xml rename {ackpine-core => ackpine-resources}/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt (100%) diff --git a/ackpine-core/build.gradle.kts b/ackpine-core/build.gradle.kts index 851494816..9679fd64e 100644 --- a/ackpine-core/build.gradle.kts +++ b/ackpine-core/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Ilya Fomichev + * Copyright (C) 2023-2024 Ilya Fomichev * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ dependencies { api(androidx.annotation) api(androidx.startup) api(libs.listenablefuture) + api(projects.ackpineResources) implementation(projects.ackpineRuntime) implementation(androidx.concurrent.futures.core) implementation(androidx.core.ktx) diff --git a/ackpine-core/consumer-rules.pro b/ackpine-core/consumer-rules.pro index 71acb4365..7372a1911 100644 --- a/ackpine-core/consumer-rules.pro +++ b/ackpine-core/consumer-rules.pro @@ -1,9 +1,4 @@ # Serializable --keep class ru.solrudev.ackpine.session.parameters.ResolvableString { *; } --keep class ru.solrudev.ackpine.session.parameters.ResolvableString$* { *; } --keep class * extends ru.solrudev.ackpine.session.parameters.ResolvableString$Resource --keep class ru.solrudev.ackpine.session.parameters.Empty { *; } --keep class ru.solrudev.ackpine.session.parameters.Raw { *; } -keep class ru.solrudev.ackpine.installer.InstallFailure { *; } -keep class ru.solrudev.ackpine.installer.InstallFailure$* { *; } -keep class ru.solrudev.ackpine.uninstaller.UninstallFailure { *; } diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt index aebae5d26..b10c404a0 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt @@ -36,7 +36,7 @@ import ru.solrudev.ackpine.installer.parameters.InstallerType import ru.solrudev.ackpine.session.Progress import ru.solrudev.ackpine.session.ProgressSession import ru.solrudev.ackpine.session.Session -import ru.solrudev.ackpine.session.parameters.DefaultNotificationString +import ru.solrudev.ackpine.session.parameters.DEFAULT_NOTIFICATION_STRING import ru.solrudev.ackpine.session.parameters.NotificationData import ru.solrudev.ackpine.session.parameters.ResolvableString import java.util.UUID @@ -109,10 +109,10 @@ internal class InstallSessionFactoryImpl internal constructor( private fun NotificationData.resolveDefault(name: String): NotificationData = NotificationData.Builder() .setTitle( - title.takeUnless { it is DefaultNotificationString } ?: AckpinePromptInstallTitle + title.takeUnless { it === DEFAULT_NOTIFICATION_STRING } ?: AckpinePromptInstallTitle ) .setContentText( - contentText.takeUnless { it is DefaultNotificationString } ?: resolveDefaultContentText(name) + contentText.takeUnless { it === DEFAULT_NOTIFICATION_STRING } ?: resolveDefaultContentText(name) ) .setIcon(icon) .build() diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt index 00d84b4b2..ce3b2290f 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt @@ -26,7 +26,7 @@ import ru.solrudev.ackpine.impl.database.dao.SessionFailureDao import ru.solrudev.ackpine.impl.uninstaller.helpers.getApplicationLabel import ru.solrudev.ackpine.impl.uninstaller.session.UninstallSession import ru.solrudev.ackpine.session.Session -import ru.solrudev.ackpine.session.parameters.DefaultNotificationString +import ru.solrudev.ackpine.session.parameters.DEFAULT_NOTIFICATION_STRING import ru.solrudev.ackpine.session.parameters.NotificationData import ru.solrudev.ackpine.session.parameters.ResolvableString import ru.solrudev.ackpine.uninstaller.UninstallFailure @@ -75,10 +75,10 @@ internal class UninstallSessionFactoryImpl internal constructor( private fun NotificationData.resolveDefault(packageName: String): NotificationData = NotificationData.Builder() .setTitle( - title.takeUnless { it is DefaultNotificationString } ?: AckpinePromptUninstallTitle + title.takeUnless { it === DEFAULT_NOTIFICATION_STRING } ?: AckpinePromptUninstallTitle ) .setContentText( - contentText.takeUnless { it is DefaultNotificationString } ?: resolveDefaultContentText(packageName) + contentText.takeUnless { it === DEFAULT_NOTIFICATION_STRING } ?: resolveDefaultContentText(packageName) ) .setIcon(icon) .build() diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt index ac591822b..4b06ff472 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt @@ -16,7 +16,6 @@ package ru.solrudev.ackpine.session.parameters -import android.content.Context import androidx.annotation.DrawableRes import androidx.annotation.RestrictTo @@ -75,8 +74,8 @@ public class NotificationData private constructor( @JvmField public val DEFAULT: NotificationData = NotificationData( icon = android.R.drawable.ic_dialog_alert, - title = DefaultNotificationString, - contentText = DefaultNotificationString + title = DEFAULT_NOTIFICATION_STRING, + contentText = DEFAULT_NOTIFICATION_STRING ) } @@ -139,7 +138,5 @@ public class NotificationData private constructor( } @RestrictTo(RestrictTo.Scope.LIBRARY) -internal data object DefaultNotificationString : ResolvableString { - private const val serialVersionUID = 809543744617543082L - override fun resolve(context: Context): String = "" -} \ No newline at end of file +@get:JvmSynthetic +internal val DEFAULT_NOTIFICATION_STRING = ResolvableString.raw("ACKPINE_DEFAULT_NOTIFICATION_STRING") \ No newline at end of file diff --git a/ackpine-resources/build.gradle.kts b/ackpine-resources/build.gradle.kts new file mode 100644 index 000000000..34663e124 --- /dev/null +++ b/ackpine-resources/build.gradle.kts @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +description = "Abstractions for resolvable and persistable Android resources" + +plugins { + id("ru.solrudev.ackpine.library") + id("ru.solrudev.ackpine.library-publish") +} + +ackpine { + id = "resources" + artifact { + name = "Ackpine Resources" + } +} + +dependencies { + api(androidx.annotation) +} \ No newline at end of file diff --git a/ackpine-resources/consumer-rules.pro b/ackpine-resources/consumer-rules.pro new file mode 100644 index 000000000..98ad18c4a --- /dev/null +++ b/ackpine-resources/consumer-rules.pro @@ -0,0 +1,6 @@ +# Serializable +-keep class ru.solrudev.ackpine.session.parameters.ResolvableString { *; } +-keep class ru.solrudev.ackpine.session.parameters.ResolvableString$* { *; } +-keep class * extends ru.solrudev.ackpine.session.parameters.ResolvableString$Resource +-keep class ru.solrudev.ackpine.session.parameters.Empty { *; } +-keep class ru.solrudev.ackpine.session.parameters.Raw { *; } \ No newline at end of file diff --git a/ackpine-resources/proguard-rules.pro b/ackpine-resources/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/ackpine-resources/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/ackpine-resources/src/main/AndroidManifest.xml b/ackpine-resources/src/main/AndroidManifest.xml new file mode 100644 index 000000000..2e0de6087 --- /dev/null +++ b/ackpine-resources/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt similarity index 100% rename from ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt rename to ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt diff --git a/sample-java/build.gradle.kts b/sample-java/build.gradle.kts index 6c948d83b..96c103521 100644 --- a/sample-java/build.gradle.kts +++ b/sample-java/build.gradle.kts @@ -64,6 +64,7 @@ android { dependencies { implementation(projects.ackpineCore) implementation(projects.ackpineSplits) + implementation(projects.ackpineResources) implementation(androidx.activity) implementation(androidx.appcompat) implementation(androidx.recyclerview) diff --git a/sample-ktx/build.gradle.kts b/sample-ktx/build.gradle.kts index 06d72baa4..560f84f68 100644 --- a/sample-ktx/build.gradle.kts +++ b/sample-ktx/build.gradle.kts @@ -70,6 +70,7 @@ tasks.withType().configureEach { dependencies { implementation(projects.ackpineSplits) implementation(projects.ackpineKtx) + implementation(projects.ackpineResources) implementation(androidx.activity) implementation(androidx.appcompat) implementation(androidx.recyclerview) diff --git a/settings.gradle.kts b/settings.gradle.kts index e9d61eff1..7f9a853ed 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -56,5 +56,6 @@ include(":ackpine-ktx") include(":ackpine-splits") include(":ackpine-assets") include(":ackpine-runtime") +include(":ackpine-resources") include(":sample-java") include(":sample-ktx") \ No newline at end of file From 700d431e28f5187a0894a8397bfb29e19023c56c Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 14:40:17 +0500 Subject: [PATCH 21/40] move ResolvableString to another package --- .../ackpine/impl/database/converters/Converters.kt | 4 ++-- .../ackpine/impl/database/model/SessionEntity.kt | 2 +- .../ackpine/impl/installer/InstallSessionFactory.kt | 2 +- .../impl/uninstaller/UninstallSessionFactory.kt | 2 +- .../ackpine/session/parameters/NotificationData.kt | 1 + .../ackpine/session/parameters/NotificationDataDsl.kt | 1 + ackpine-resources/consumer-rules.pro | 10 +++++----- .../parameters => resources}/ResolvableString.kt | 2 +- .../ackpine/sample/install/InstallSessionsAdapter.java | 2 +- .../ackpine/sample/install/InstallViewModel.java | 2 +- .../solrudev/ackpine/sample/install/SessionData.java | 2 +- .../ackpine/sample/install/SessionDataRepository.java | 2 +- .../sample/install/SessionDataRepositoryImpl.java | 2 +- .../ackpine/sample/install/InstallSessionsAdapter.kt | 2 +- .../solrudev/ackpine/sample/install/InstallUiState.kt | 2 +- .../ackpine/sample/install/InstallViewModel.kt | 2 +- .../ru/solrudev/ackpine/sample/install/SessionData.kt | 2 +- .../ackpine/sample/install/SessionDataRepository.kt | 2 +- .../sample/install/SessionDataRepositoryImpl.kt | 2 +- 19 files changed, 24 insertions(+), 22 deletions(-) rename ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/{session/parameters => resources}/ResolvableString.kt (99%) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/converters/Converters.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/converters/Converters.kt index 8a38807d1..f239579c8 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/converters/Converters.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/converters/Converters.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Ilya Fomichev + * Copyright (C) 2023-2024 Ilya Fomichev * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ package ru.solrudev.ackpine.impl.database.converters import androidx.room.TypeConverter import ru.solrudev.ackpine.installer.InstallFailure -import ru.solrudev.ackpine.session.parameters.ResolvableString +import ru.solrudev.ackpine.resources.ResolvableString import ru.solrudev.ackpine.uninstaller.UninstallFailure internal object NotificationStringConverters { diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/model/SessionEntity.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/model/SessionEntity.kt index e4ecd31c8..9060802b4 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/model/SessionEntity.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/model/SessionEntity.kt @@ -24,8 +24,8 @@ import androidx.room.Entity import androidx.room.PrimaryKey import androidx.room.Relation import ru.solrudev.ackpine.installer.parameters.InstallerType +import ru.solrudev.ackpine.resources.ResolvableString import ru.solrudev.ackpine.session.parameters.Confirmation -import ru.solrudev.ackpine.session.parameters.ResolvableString @RestrictTo(RestrictTo.Scope.LIBRARY) @Entity(tableName = "sessions") diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt index b10c404a0..524b4299d 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt @@ -33,12 +33,12 @@ import ru.solrudev.ackpine.impl.installer.session.SessionBasedInstallSession import ru.solrudev.ackpine.installer.InstallFailure import ru.solrudev.ackpine.installer.parameters.InstallParameters import ru.solrudev.ackpine.installer.parameters.InstallerType +import ru.solrudev.ackpine.resources.ResolvableString import ru.solrudev.ackpine.session.Progress import ru.solrudev.ackpine.session.ProgressSession import ru.solrudev.ackpine.session.Session import ru.solrudev.ackpine.session.parameters.DEFAULT_NOTIFICATION_STRING import ru.solrudev.ackpine.session.parameters.NotificationData -import ru.solrudev.ackpine.session.parameters.ResolvableString import java.util.UUID import java.util.concurrent.Executor diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt index ce3b2290f..a6ee08a5f 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt @@ -25,10 +25,10 @@ import ru.solrudev.ackpine.impl.database.dao.SessionDao import ru.solrudev.ackpine.impl.database.dao.SessionFailureDao import ru.solrudev.ackpine.impl.uninstaller.helpers.getApplicationLabel import ru.solrudev.ackpine.impl.uninstaller.session.UninstallSession +import ru.solrudev.ackpine.resources.ResolvableString import ru.solrudev.ackpine.session.Session import ru.solrudev.ackpine.session.parameters.DEFAULT_NOTIFICATION_STRING import ru.solrudev.ackpine.session.parameters.NotificationData -import ru.solrudev.ackpine.session.parameters.ResolvableString import ru.solrudev.ackpine.uninstaller.UninstallFailure import ru.solrudev.ackpine.uninstaller.parameters.UninstallParameters import java.util.UUID diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt index 4b06ff472..ab4fb96d4 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt @@ -18,6 +18,7 @@ package ru.solrudev.ackpine.session.parameters import androidx.annotation.DrawableRes import androidx.annotation.RestrictTo +import ru.solrudev.ackpine.resources.ResolvableString /** * Data for a high-priority notification which launches confirmation activity. diff --git a/ackpine-ktx/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationDataDsl.kt b/ackpine-ktx/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationDataDsl.kt index 4a2d6daa3..12e7a3fd9 100644 --- a/ackpine-ktx/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationDataDsl.kt +++ b/ackpine-ktx/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationDataDsl.kt @@ -18,6 +18,7 @@ package ru.solrudev.ackpine.session.parameters import android.annotation.SuppressLint import androidx.annotation.DrawableRes +import ru.solrudev.ackpine.resources.ResolvableString /** * DSL allowing to configure [high-priority notification for Session confirmation][NotificationData]. diff --git a/ackpine-resources/consumer-rules.pro b/ackpine-resources/consumer-rules.pro index 98ad18c4a..c2d5fee37 100644 --- a/ackpine-resources/consumer-rules.pro +++ b/ackpine-resources/consumer-rules.pro @@ -1,6 +1,6 @@ # Serializable --keep class ru.solrudev.ackpine.session.parameters.ResolvableString { *; } --keep class ru.solrudev.ackpine.session.parameters.ResolvableString$* { *; } --keep class * extends ru.solrudev.ackpine.session.parameters.ResolvableString$Resource --keep class ru.solrudev.ackpine.session.parameters.Empty { *; } --keep class ru.solrudev.ackpine.session.parameters.Raw { *; } \ No newline at end of file +-keep class ru.solrudev.ackpine.resources.ResolvableString { *; } +-keep class ru.solrudev.ackpine.resources.ResolvableString$* { *; } +-keep class * extends ru.solrudev.ackpine.resources.ResolvableString$Resource +-keep class ru.solrudev.ackpine.resources.Empty { *; } +-keep class ru.solrudev.ackpine.resources.Raw { *; } \ No newline at end of file diff --git a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt similarity index 99% rename from ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt rename to ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt index d5b652495..18682887d 100644 --- a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/session/parameters/ResolvableString.kt +++ b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt @@ -16,7 +16,7 @@ @file:Suppress("Unused") -package ru.solrudev.ackpine.session.parameters +package ru.solrudev.ackpine.resources import android.content.Context import androidx.annotation.StringRes diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java index d9f91cc4d..915cb0082 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.java @@ -38,10 +38,10 @@ import java.util.Objects; import java.util.UUID; +import ru.solrudev.ackpine.resources.ResolvableString; import ru.solrudev.ackpine.sample.R; import ru.solrudev.ackpine.sample.databinding.ItemInstallSessionBinding; import ru.solrudev.ackpine.session.Progress; -import ru.solrudev.ackpine.session.parameters.ResolvableString; public final class InstallSessionsAdapter extends ListAdapter { diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java index a95cacd4e..9a70e53ce 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java @@ -51,11 +51,11 @@ import ru.solrudev.ackpine.installer.InstallFailure; import ru.solrudev.ackpine.installer.PackageInstaller; import ru.solrudev.ackpine.installer.parameters.InstallParameters; +import ru.solrudev.ackpine.resources.ResolvableString; import ru.solrudev.ackpine.sample.R; import ru.solrudev.ackpine.session.Failure; import ru.solrudev.ackpine.session.ProgressSession; import ru.solrudev.ackpine.session.Session; -import ru.solrudev.ackpine.session.parameters.ResolvableString; import ru.solrudev.ackpine.splits.Apk; public final class InstallViewModel extends ViewModel { diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionData.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionData.java index 1b2e9950a..2b5c41b9d 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionData.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionData.java @@ -22,7 +22,7 @@ import java.io.Serializable; import java.util.UUID; -import ru.solrudev.ackpine.session.parameters.ResolvableString; +import ru.solrudev.ackpine.resources.ResolvableString; public record SessionData(@NonNull UUID id, @NonNull String name, diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepository.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepository.java index 5955fbc8d..dcd085ac5 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepository.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepository.java @@ -22,8 +22,8 @@ import java.util.List; import java.util.UUID; +import ru.solrudev.ackpine.resources.ResolvableString; import ru.solrudev.ackpine.session.Progress; -import ru.solrudev.ackpine.session.parameters.ResolvableString; public interface SessionDataRepository { diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.java index 5d06fdd71..5b16e8d26 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.java @@ -26,8 +26,8 @@ import java.util.Objects; import java.util.UUID; +import ru.solrudev.ackpine.resources.ResolvableString; import ru.solrudev.ackpine.session.Progress; -import ru.solrudev.ackpine.session.parameters.ResolvableString; public final class SessionDataRepositoryImpl implements SessionDataRepository { diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.kt index 2cc6ce5cc..f19e8ca6c 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallSessionsAdapter.kt @@ -28,10 +28,10 @@ import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import androidx.transition.Fade import androidx.transition.TransitionManager +import ru.solrudev.ackpine.resources.ResolvableString import ru.solrudev.ackpine.sample.R import ru.solrudev.ackpine.sample.databinding.ItemInstallSessionBinding import ru.solrudev.ackpine.session.Progress -import ru.solrudev.ackpine.session.parameters.ResolvableString import java.util.UUID class InstallSessionsAdapter( diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallUiState.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallUiState.kt index ed38d33a8..3621e028a 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallUiState.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallUiState.kt @@ -16,7 +16,7 @@ package ru.solrudev.ackpine.sample.install -import ru.solrudev.ackpine.session.parameters.ResolvableString +import ru.solrudev.ackpine.resources.ResolvableString data class InstallUiState( val error: ResolvableString = ResolvableString.empty(), diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt index f5f51c74a..0b63a3a9c 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt @@ -48,12 +48,12 @@ import ru.solrudev.ackpine.installer.InstallFailure import ru.solrudev.ackpine.installer.PackageInstaller import ru.solrudev.ackpine.installer.createSession import ru.solrudev.ackpine.installer.getSession +import ru.solrudev.ackpine.resources.ResolvableString import ru.solrudev.ackpine.sample.R import ru.solrudev.ackpine.session.ProgressSession import ru.solrudev.ackpine.session.Session import ru.solrudev.ackpine.session.SessionResult import ru.solrudev.ackpine.session.await -import ru.solrudev.ackpine.session.parameters.ResolvableString import ru.solrudev.ackpine.session.progress import ru.solrudev.ackpine.session.state import ru.solrudev.ackpine.splits.Apk diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionData.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionData.kt index b90a69ee1..e41cb061a 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionData.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionData.kt @@ -16,7 +16,7 @@ package ru.solrudev.ackpine.sample.install -import ru.solrudev.ackpine.session.parameters.ResolvableString +import ru.solrudev.ackpine.resources.ResolvableString import java.io.Serializable import java.util.UUID diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepository.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepository.kt index 797cc2eb3..8e18c7fd6 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepository.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepository.kt @@ -17,8 +17,8 @@ package ru.solrudev.ackpine.sample.install import kotlinx.coroutines.flow.StateFlow +import ru.solrudev.ackpine.resources.ResolvableString import ru.solrudev.ackpine.session.Progress -import ru.solrudev.ackpine.session.parameters.ResolvableString import java.util.UUID interface SessionDataRepository { diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.kt index c5bb7d02d..675908840 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/SessionDataRepositoryImpl.kt @@ -17,8 +17,8 @@ package ru.solrudev.ackpine.sample.install import androidx.lifecycle.SavedStateHandle +import ru.solrudev.ackpine.resources.ResolvableString import ru.solrudev.ackpine.session.Progress -import ru.solrudev.ackpine.session.parameters.ResolvableString import java.util.UUID private const val SESSIONS_KEY = "SESSIONS" From 257ada47f402490c84fe1d20ce4c7d210c593222 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 14:44:04 +0500 Subject: [PATCH 22/40] ResolvableString.empty() docs --- .../kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt index 18682887d..9d60c4e26 100644 --- a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt +++ b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt @@ -53,7 +53,7 @@ public sealed interface ResolvableString : Serializable { public companion object { /** - * Creates an empty [ResolvableString]. + * Returns an empty [ResolvableString]. */ @JvmStatic public fun empty(): ResolvableString = Empty From 0940f18762ead14a3a19757064a7c2c9dafd7a07 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 15:19:29 +0500 Subject: [PATCH 23/40] ensure stable notification icon resources --- .../8.json | 8 ++-- .../ackpine/impl/database/AckpineDatabase.kt | 10 ++++- .../database/AckpineDatabaseMigrations.kt | 7 +++- .../impl/database/converters/Converters.kt | 14 ++++++- .../impl/database/model/SessionEntity.kt | 5 +-- .../session/helpers/LaunchConfirmation.kt | 2 +- .../session/parameters/NotificationData.kt | 37 +++++++++++++++---- .../session/parameters/NotificationDataDsl.kt | 9 +---- .../ackpine/resources/ResolvableString.kt | 2 +- docs/configuration.md | 19 +++++++++- 10 files changed, 83 insertions(+), 30 deletions(-) diff --git a/ackpine-core/schemas/ru.solrudev.ackpine.impl.database.AckpineDatabase/8.json b/ackpine-core/schemas/ru.solrudev.ackpine.impl.database.AckpineDatabase/8.json index 9dcf88230..a8ead51d0 100644 --- a/ackpine-core/schemas/ru.solrudev.ackpine.impl.database.AckpineDatabase/8.json +++ b/ackpine-core/schemas/ru.solrudev.ackpine.impl.database.AckpineDatabase/8.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 8, - "identityHash": "70a447048443686563989e6b938d53cc", + "identityHash": "22d39a00bd2f00f0739bd5c264abfda9", "entities": [ { "tableName": "sessions", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT NOT NULL, `state` TEXT NOT NULL, `confirmation` TEXT NOT NULL, `notification_title` BLOB NOT NULL, `notification_text` BLOB NOT NULL, `notification_icon` INTEGER NOT NULL, `require_user_action` INTEGER NOT NULL DEFAULT true, `last_launch_timestamp` INTEGER NOT NULL DEFAULT 0, `last_commit_timestamp` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `type` TEXT NOT NULL, `state` TEXT NOT NULL, `confirmation` TEXT NOT NULL, `notification_title` BLOB NOT NULL, `notification_text` BLOB NOT NULL, `notification_icon` BLOB NOT NULL, `require_user_action` INTEGER NOT NULL DEFAULT true, `last_launch_timestamp` INTEGER NOT NULL DEFAULT 0, `last_commit_timestamp` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -47,7 +47,7 @@ { "fieldPath": "notificationIcon", "columnName": "notification_icon", - "affinity": "INTEGER", + "affinity": "BLOB", "notNull": true }, { @@ -580,7 +580,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, '70a447048443686563989e6b938d53cc')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '22d39a00bd2f00f0739bd5c264abfda9')" ] } } \ No newline at end of file diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabase.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabase.kt index 66b68918e..633c91de5 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabase.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabase.kt @@ -26,8 +26,9 @@ import androidx.room.TypeConverters import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteOpenHelper import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import ru.solrudev.ackpine.impl.database.converters.DrawableIdConverters import ru.solrudev.ackpine.impl.database.converters.InstallFailureConverters -import ru.solrudev.ackpine.impl.database.converters.NotificationStringConverters +import ru.solrudev.ackpine.impl.database.converters.ResolvableStringConverters import ru.solrudev.ackpine.impl.database.converters.UninstallFailureConverters import ru.solrudev.ackpine.impl.database.dao.InstallSessionDao import ru.solrudev.ackpine.impl.database.dao.LastUpdateTimestampDao @@ -83,7 +84,12 @@ private const val PURGE_SQL = "DELETE FROM sessions WHERE state IN $TERMINAL_STA exportSchema = true ) @TypeConverters( - value = [InstallFailureConverters::class, UninstallFailureConverters::class, NotificationStringConverters::class] + value = [ + InstallFailureConverters::class, + UninstallFailureConverters::class, + ResolvableStringConverters::class, + DrawableIdConverters::class + ] ) internal abstract class AckpineDatabase : RoomDatabase() { diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabaseMigrations.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabaseMigrations.kt index 1e8ee076c..ae69aafe7 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabaseMigrations.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/AckpineDatabaseMigrations.kt @@ -44,7 +44,12 @@ internal object Migration_4_5 : Migration(4, 5) { @RestrictTo(RestrictTo.Scope.LIBRARY) internal object Migration_7_8 : Migration(7, 8) { override fun migrate(db: SupportSQLiteDatabase) = db.migrate { - execSQL("DELETE FROM sessions") + execSQL("DROP TABLE sessions") + execSQL("CREATE TABLE IF NOT EXISTS sessions (id TEXT NOT NULL, type TEXT NOT NULL, state TEXT NOT NULL, confirmation TEXT NOT NULL, notification_title BLOB NOT NULL, notification_text BLOB NOT NULL, notification_icon BLOB NOT NULL, require_user_action INTEGER NOT NULL DEFAULT true, last_launch_timestamp INTEGER NOT NULL DEFAULT 0, last_commit_timestamp INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(id))") + execSQL("CREATE INDEX IF NOT EXISTS index_sessions_type ON sessions (type)") + execSQL("CREATE INDEX IF NOT EXISTS index_sessions_state ON sessions (state)") + execSQL("CREATE INDEX IF NOT EXISTS index_sessions_last_launch_timestamp ON sessions (last_launch_timestamp)") + execSQL("CREATE INDEX IF NOT EXISTS index_sessions_last_commit_timestamp ON sessions (last_commit_timestamp)") execSQL("DELETE FROM sessions_installer_types") execSQL("DELETE FROM sessions_install_failures") execSQL("DELETE FROM sessions_uninstall_failures") diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/converters/Converters.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/converters/Converters.kt index f239579c8..43bd1b724 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/converters/Converters.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/converters/Converters.kt @@ -19,9 +19,21 @@ package ru.solrudev.ackpine.impl.database.converters import androidx.room.TypeConverter import ru.solrudev.ackpine.installer.InstallFailure import ru.solrudev.ackpine.resources.ResolvableString +import ru.solrudev.ackpine.session.parameters.DrawableId import ru.solrudev.ackpine.uninstaller.UninstallFailure -internal object NotificationStringConverters { +internal object DrawableIdConverters { + + @TypeConverter + @JvmStatic + internal fun fromByteArray(byteArray: ByteArray): DrawableId = byteArray.deserialize() + + @TypeConverter + @JvmStatic + internal fun toByteArray(drawableId: DrawableId): ByteArray = drawableId.serialize() +} + +internal object ResolvableStringConverters { @TypeConverter @JvmStatic diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/model/SessionEntity.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/model/SessionEntity.kt index 9060802b4..11eb09487 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/model/SessionEntity.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/database/model/SessionEntity.kt @@ -16,7 +16,6 @@ package ru.solrudev.ackpine.impl.database.model -import androidx.annotation.DrawableRes import androidx.annotation.RestrictTo import androidx.room.ColumnInfo import androidx.room.Embedded @@ -26,6 +25,7 @@ import androidx.room.Relation import ru.solrudev.ackpine.installer.parameters.InstallerType import ru.solrudev.ackpine.resources.ResolvableString import ru.solrudev.ackpine.session.parameters.Confirmation +import ru.solrudev.ackpine.session.parameters.DrawableId @RestrictTo(RestrictTo.Scope.LIBRARY) @Entity(tableName = "sessions") @@ -43,9 +43,8 @@ internal data class SessionEntity internal constructor( val notificationTitle: ResolvableString, @ColumnInfo(name = "notification_text") val notificationText: ResolvableString, - @DrawableRes @ColumnInfo(name = "notification_icon") - val notificationIcon: Int, + val notificationIcon: DrawableId, @ColumnInfo(name = "require_user_action", defaultValue = "true") val requireUserAction: Boolean, @ColumnInfo(name = "last_launch_timestamp", defaultValue = "0", index = true) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/helpers/LaunchConfirmation.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/helpers/LaunchConfirmation.kt index f82d0db5d..0d85dfa45 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/helpers/LaunchConfirmation.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/helpers/LaunchConfirmation.kt @@ -126,7 +126,7 @@ private fun Context.showNotification( setContentIntent(intent) priority = NotificationCompat.PRIORITY_MAX setDefaults(NotificationCompat.DEFAULT_ALL) - setSmallIcon(notificationData.icon) + setSmallIcon(notificationData.icon.drawableId) setOngoing(true) setAutoCancel(true) }.build() diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt index ab4fb96d4..e839311aa 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt @@ -19,6 +19,8 @@ package ru.solrudev.ackpine.session.parameters import androidx.annotation.DrawableRes import androidx.annotation.RestrictTo import ru.solrudev.ackpine.resources.ResolvableString +import java.io.Serializable +import android.graphics.drawable.Drawable /** * Data for a high-priority notification which launches confirmation activity. @@ -30,7 +32,7 @@ public class NotificationData private constructor( * * Default value is [android.R.drawable.ic_dialog_alert]. */ - @DrawableRes public val icon: Int, + public val icon: DrawableId, /** * Notification title. @@ -51,16 +53,16 @@ public class NotificationData private constructor( if (this === other) return true if (javaClass != other?.javaClass) return false other as NotificationData + if (icon != other.icon) return false if (title != other.title) return false if (contentText != other.contentText) return false - if (icon != other.icon) return false return true } override fun hashCode(): Int { - var result = title.hashCode() + var result = icon.hashCode() + result = 31 * result + title.hashCode() result = 31 * result + contentText.hashCode() - result = 31 * result + icon return result } @@ -74,7 +76,7 @@ public class NotificationData private constructor( */ @JvmField public val DEFAULT: NotificationData = NotificationData( - icon = android.R.drawable.ic_dialog_alert, + icon = DefaultNotificationIcon, title = DEFAULT_NOTIFICATION_STRING, contentText = DEFAULT_NOTIFICATION_STRING ) @@ -90,8 +92,7 @@ public class NotificationData private constructor( * * Default value is [android.R.drawable.ic_dialog_alert]. */ - @DrawableRes - public var icon: Int = DEFAULT.icon + public var icon: DrawableId = DEFAULT.icon private set /** @@ -113,7 +114,7 @@ public class NotificationData private constructor( /** * Sets [NotificationData.icon]. */ - public fun setIcon(@DrawableRes icon: Int): Builder = apply { + public fun setIcon(icon: DrawableId): Builder = apply { this.icon = icon } @@ -138,6 +139,26 @@ public class NotificationData private constructor( } } +/** + * [Drawable] represented by Android resource ID. + * + * Should be explicitly subclassed to ensure stable persistence, and `serialVersionUID` must be present. Example: + * ``` + * object InstallIcon : DrawableId(R.drawable.ic_install) { + * private const val serialVersionUID = 3692803605642002954L + * } + * ``` + */ +public abstract class DrawableId(@[DrawableRes Transient] public val drawableId: Int) : Serializable { + private companion object { + private const val serialVersionUID = 6564416758029834576L + } +} + +private object DefaultNotificationIcon : DrawableId(android.R.drawable.ic_dialog_alert) { + private const val serialVersionUID = 6906923061913799903L +} + @RestrictTo(RestrictTo.Scope.LIBRARY) @get:JvmSynthetic internal val DEFAULT_NOTIFICATION_STRING = ResolvableString.raw("ACKPINE_DEFAULT_NOTIFICATION_STRING") \ No newline at end of file diff --git a/ackpine-ktx/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationDataDsl.kt b/ackpine-ktx/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationDataDsl.kt index 12e7a3fd9..c6cd7ccc7 100644 --- a/ackpine-ktx/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationDataDsl.kt +++ b/ackpine-ktx/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationDataDsl.kt @@ -16,8 +16,6 @@ package ru.solrudev.ackpine.session.parameters -import android.annotation.SuppressLint -import androidx.annotation.DrawableRes import ru.solrudev.ackpine.resources.ResolvableString /** @@ -31,10 +29,7 @@ public interface NotificationDataDsl { * * Default value is [android.R.drawable.ic_dialog_alert]. */ - @set:SuppressLint("SupportAnnotationUsage") - @get:DrawableRes - @set:DrawableRes - public var icon: Int + public var icon: DrawableId /** * Notification title. @@ -56,7 +51,7 @@ internal class NotificationDataDslBuilder : NotificationDataDsl { private val builder = NotificationData.Builder() - override var icon: Int + override var icon: DrawableId get() = builder.icon set(value) { builder.setIcon(value) diff --git a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt index 9d60c4e26..89695ddf0 100644 --- a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt +++ b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt @@ -99,7 +99,7 @@ public sealed interface ResolvableString : Serializable { * [ResolvableString] represented by Android resource string with optional arguments. Arguments can be * [ResolvableStrings][ResolvableString] as well. * - * Should be explicitly subclassed to ensure stable persistence. Example: + * Should be explicitly subclassed to ensure stable persistence, and `serialVersionUID` must be present. Example: * ``` * object InstallMessageTitle : ResolvableString.Resource(R.string.install_message_title) { * private const val serialVersionUID = -1310602635578779088L diff --git a/docs/configuration.md b/docs/configuration.md index 561f6e4db..17f155da1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -23,7 +23,7 @@ An example of creating a session with custom parameters: notification { title = InstallMessageTitle contentText = InstallMessage(fileName) - icon = R.drawable.ic_install + icon = InstallIcon } } @@ -36,6 +36,10 @@ An example of creating a session with custom parameters: private const val serialVersionUID = 4749568844072243110L } } + + object InstallIcon : DrawableId(R.drawable.ic_install) { + private const val serialVersionUID = 3692803605642002954L + } ``` === "Java" @@ -51,13 +55,14 @@ An example of creating a session with custom parameters: .setNotificationData(new NotificationData.Builder() .setTitle(Resources.INSTALL_MESSAGE_TITLE) .setContentText(new Resources.InstallMessage(fileName)) - .setIcon(R.drawable.ic_install) + .setIcon(Resources.INSTALL_ICON) .build()) .build()); public class Resources { public static final ResolvableString INSTALL_MESSAGE_TITLE = new InstallMessageTitle(); + public static final DrawableId INSTALL_ICON = new InstallIcon(); private static class InstallMessageTitle extends ResolvableString.Resource { @@ -78,6 +83,16 @@ An example of creating a session with custom parameters: super(R.string.install_message, fileName); } } + + private static class InstallIcon extends DrawableId { + + @Serial + private static final long serialVersionUID = 3692803605642002954L; + + public InstallIcon() { + super(R.drawable.ic_install); + } + } private Resources() { } From e38fc09ecfdeddc51f90a81aac59aeb03bd6e699 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 15:20:10 +0500 Subject: [PATCH 24/40] increase version --- README.md | 2 +- docs/index.md | 2 +- version.properties | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 53323df5d..4f5ffa4df 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Ackpine depends on Jetpack libraries, so it's necessary to declare the `google() ```kotlin dependencies { - val ackpineVersion = "0.7.6" + val ackpineVersion = "0.8.0" implementation("ru.solrudev.ackpine:ackpine-core:$ackpineVersion") // optional - Kotlin extensions and Coroutines support diff --git a/docs/index.md b/docs/index.md index e421d94d7..cec0269b0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,7 +27,7 @@ Ackpine depends on Jetpack libraries, so it's necessary to declare the `google() ```kotlin dependencies { - val ackpineVersion = "0.7.6" + val ackpineVersion = "0.8.0" implementation("ru.solrudev.ackpine:ackpine-core:$ackpineVersion") // optional - Kotlin extensions and Coroutines support diff --git a/version.properties b/version.properties index 893c010db..6b81380b9 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ MAJOR_VERSION=0 -MINOR_VERSION=7 -PATCH_VERSION=6 +MINOR_VERSION=8 +PATCH_VERSION=0 SUFFIX= SNAPSHOT=false \ No newline at end of file From 57e60063357d493037bdd27ef86b195abb51b059 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 16:18:08 +0500 Subject: [PATCH 25/40] fix ResolvableString.resource() creating incorrect Resource object --- .../kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt index 89695ddf0..f64b46451 100644 --- a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt +++ b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt @@ -91,7 +91,7 @@ public sealed interface ResolvableString : Serializable { @Suppress("serial") @JvmStatic public fun resource(@StringRes stringId: Int, vararg args: Serializable): ResolvableString { - return object : Resource(stringId, args) {} + return object : Resource(stringId, *args) {} } } From 883172db561b211bbb9ba23473c8e3da73d97718 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 16:18:27 +0500 Subject: [PATCH 26/40] fix class name clash --- .../ru/solrudev/ackpine/session/parameters/NotificationData.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt index e839311aa..f463f0534 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:JvmName("NotificationDataConstants") + package ru.solrudev.ackpine.session.parameters import androidx.annotation.DrawableRes From bb5a7f5006c7d9a18e4733e8441cdfaacc887b3e Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 16:32:06 +0500 Subject: [PATCH 27/40] fix persisting unresolved default ResolvableString --- .../impl/installer/InstallSessionFactory.kt | 26 ++++++----- .../impl/installer/PackageInstallerImpl.kt | 44 ++++++++++--------- .../uninstaller/PackageUninstallerImpl.kt | 10 +++-- .../uninstaller/UninstallSessionFactory.kt | 27 +++++++----- 4 files changed, 62 insertions(+), 45 deletions(-) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt index 524b4299d..5f84cabc7 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt @@ -56,6 +56,8 @@ internal interface InstallSessionFactory { lastUpdateTimestamp: Long = Long.MAX_VALUE, needToCompleteIfSucceeded: Boolean = false ): ProgressSession + + fun resolveNotificationData(notificationData: NotificationData, name: String): NotificationData } @RestrictTo(RestrictTo.Scope.LIBRARY) @@ -87,7 +89,7 @@ internal class InstallSessionFactoryImpl internal constructor( apk = parameters.apks.toList().singleOrNull() ?: throw SplitPackagesNotSupportedException(), id, initialState, initialProgress, parameters.confirmation, - parameters.notificationData.resolveDefault(parameters.name), + resolveNotificationData(parameters.notificationData, parameters.name), lastUpdateTimestampDao, sessionDao, sessionFailureDao, sessionProgressDao, executor, handler, notificationId, packageName, lastUpdateTimestamp, needToCompleteIfSucceeded, @@ -99,7 +101,7 @@ internal class InstallSessionFactoryImpl internal constructor( apks = parameters.apks.toList(), id, initialState, initialProgress, parameters.confirmation, - parameters.notificationData.resolveDefault(parameters.name), + resolveNotificationData(parameters.notificationData, parameters.name), parameters.requireUserAction, parameters.installMode, sessionDao, sessionFailureDao, sessionProgressDao, nativeSessionIdDao, @@ -107,15 +109,17 @@ internal class InstallSessionFactoryImpl internal constructor( ) } - private fun NotificationData.resolveDefault(name: String): NotificationData = NotificationData.Builder() - .setTitle( - title.takeUnless { it === DEFAULT_NOTIFICATION_STRING } ?: AckpinePromptInstallTitle - ) - .setContentText( - contentText.takeUnless { it === DEFAULT_NOTIFICATION_STRING } ?: resolveDefaultContentText(name) - ) - .setIcon(icon) - .build() + override fun resolveNotificationData(notificationData: NotificationData, name: String) = notificationData.run { + NotificationData.Builder() + .setTitle( + title.takeUnless { it === DEFAULT_NOTIFICATION_STRING } ?: AckpinePromptInstallTitle + ) + .setContentText( + contentText.takeUnless { it === DEFAULT_NOTIFICATION_STRING } ?: resolveDefaultContentText(name) + ) + .setIcon(icon) + .build() + } private fun resolveDefaultContentText(name: String): ResolvableString { if (name.isNotEmpty()) { diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/PackageInstallerImpl.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/PackageInstallerImpl.kt index 808ffd2e9..4c5721f14 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/PackageInstallerImpl.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/PackageInstallerImpl.kt @@ -202,7 +202,7 @@ internal class PackageInstallerImpl internal constructor( parameters: InstallParameters, dbWriteSemaphore: BinarySemaphore, notificationId: Int - ) { + ) = executor.executeWithSemaphore(dbWriteSemaphore) { var packageName: String? = null val installMode = when (parameters.installMode) { is InstallMode.Full -> InstallModeEntity.InstallMode.FULL @@ -211,27 +211,29 @@ internal class PackageInstallerImpl internal constructor( InstallModeEntity.InstallMode.INHERIT_EXISTING } } - executor.executeWithSemaphore(dbWriteSemaphore) { - installSessionDao.insertInstallSession( - SessionEntity.InstallSession( - session = SessionEntity( - id.toString(), - SessionEntity.Type.INSTALL, - SessionEntity.State.PENDING, - parameters.confirmation, - parameters.notificationData.title, - parameters.notificationData.contentText, - parameters.notificationData.icon, - parameters.requireUserAction - ), - installerType = parameters.installerType, - uris = parameters.apks.toList().map { it.toString() }, - name = parameters.name, - notificationId, installMode, packageName, - lastUpdateTimestamp = Long.MAX_VALUE - ) + val notificationData = installSessionFactory.resolveNotificationData( + parameters.notificationData, + parameters.name + ) + installSessionDao.insertInstallSession( + SessionEntity.InstallSession( + session = SessionEntity( + id.toString(), + SessionEntity.Type.INSTALL, + SessionEntity.State.PENDING, + parameters.confirmation, + notificationData.title, + notificationData.contentText, + notificationData.icon, + parameters.requireUserAction + ), + installerType = parameters.installerType, + uris = parameters.apks.toList().map { it.toString() }, + name = parameters.name, + notificationId, installMode, packageName, + lastUpdateTimestamp = Long.MAX_VALUE ) - } + ) } @SuppressLint("NewApi") diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/PackageUninstallerImpl.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/PackageUninstallerImpl.kt index 9b560a676..f082cf49f 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/PackageUninstallerImpl.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/PackageUninstallerImpl.kt @@ -139,6 +139,10 @@ internal class PackageUninstallerImpl internal constructor( dbWriteSemaphore: BinarySemaphore, notificationId: Int ) = executor.executeWithSemaphore(dbWriteSemaphore) { + val notificationData = uninstallSessionFactory.resolveNotificationData( + parameters.notificationData, + parameters.packageName + ) uninstallSessionDao.insertUninstallSession( SessionEntity.UninstallSession( session = SessionEntity( @@ -146,9 +150,9 @@ internal class PackageUninstallerImpl internal constructor( SessionEntity.Type.UNINSTALL, SessionEntity.State.PENDING, parameters.confirmation, - parameters.notificationData.title, - parameters.notificationData.contentText, - parameters.notificationData.icon, + notificationData.title, + notificationData.contentText, + notificationData.icon, requireUserAction = true ), packageName = parameters.packageName, diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt index a6ee08a5f..258df289e 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt @@ -44,6 +44,8 @@ internal interface UninstallSessionFactory { notificationId: Int, dbWriteSemaphore: BinarySemaphore ): Session + + fun resolveNotificationData(notificationData: NotificationData, packageName: String): NotificationData } @RestrictTo(RestrictTo.Scope.LIBRARY) @@ -67,21 +69,26 @@ internal class UninstallSessionFactoryImpl internal constructor( parameters.packageName, id, initialState, parameters.confirmation, - parameters.notificationData.resolveDefault(parameters.packageName), + resolveNotificationData(parameters.notificationData, parameters.packageName), sessionDao, sessionFailureDao, executor, handler, notificationId, dbWriteSemaphore ) } - private fun NotificationData.resolveDefault(packageName: String): NotificationData = NotificationData.Builder() - .setTitle( - title.takeUnless { it === DEFAULT_NOTIFICATION_STRING } ?: AckpinePromptUninstallTitle - ) - .setContentText( - contentText.takeUnless { it === DEFAULT_NOTIFICATION_STRING } ?: resolveDefaultContentText(packageName) - ) - .setIcon(icon) - .build() + override fun resolveNotificationData( + notificationData: NotificationData, + packageName: String + ) = notificationData.run { + NotificationData.Builder() + .setTitle( + title.takeUnless { it === DEFAULT_NOTIFICATION_STRING } ?: AckpinePromptUninstallTitle + ) + .setContentText( + contentText.takeUnless { it === DEFAULT_NOTIFICATION_STRING } ?: resolveDefaultContentText(packageName) + ) + .setIcon(icon) + .build() + } private fun resolveDefaultContentText(packageName: String): ResolvableString { val label = applicationContext.packageManager.getApplicationLabel(packageName)?.toString() From c8f74346d4080f3b538fdf9b6d458735a412a356 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 17:14:31 +0500 Subject: [PATCH 28/40] replace resource id field with function --- .../impl/installer/InstallSessionFactory.kt | 11 +++-- .../session/helpers/LaunchConfirmation.kt | 2 +- .../uninstaller/UninstallSessionFactory.kt | 11 +++-- .../session/parameters/NotificationData.kt | 11 +++-- .../ackpine/resources/ResolvableString.kt | 31 +++++++------- docs/configuration.md | 40 ++++++++++++------- 6 files changed, 65 insertions(+), 41 deletions(-) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt index 5f84cabc7..7023df323 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/InstallSessionFactory.kt @@ -129,16 +129,19 @@ internal class InstallSessionFactoryImpl internal constructor( } } -private object AckpinePromptInstallTitle : ResolvableString.Resource(R.string.ackpine_prompt_install_title) { +private object AckpinePromptInstallTitle : ResolvableString.Resource() { private const val serialVersionUID = 7815666924791958742L + override fun stringId() = R.string.ackpine_prompt_install_title } -private object AckpinePromptInstallMessage : ResolvableString.Resource(R.string.ackpine_prompt_install_message) { +private object AckpinePromptInstallMessage : ResolvableString.Resource() { private const val serialVersionUID = 1224637050663404482L + override fun stringId() = R.string.ackpine_prompt_install_message } -private class AckpinePromptInstallMessageWithLabel(name: String) : - ResolvableString.Resource(R.string.ackpine_prompt_install_message_with_label, name) { +private class AckpinePromptInstallMessageWithLabel(name: String) : ResolvableString.Resource(name) { + + override fun stringId() = R.string.ackpine_prompt_install_message_with_label private companion object { private const val serialVersionUID = -6931607904159775056L diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/helpers/LaunchConfirmation.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/helpers/LaunchConfirmation.kt index 0d85dfa45..136891f73 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/helpers/LaunchConfirmation.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/session/helpers/LaunchConfirmation.kt @@ -126,7 +126,7 @@ private fun Context.showNotification( setContentIntent(intent) priority = NotificationCompat.PRIORITY_MAX setDefaults(NotificationCompat.DEFAULT_ALL) - setSmallIcon(notificationData.icon.drawableId) + setSmallIcon(notificationData.icon.drawableId()) setOngoing(true) setAutoCancel(true) }.build() diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt index 258df289e..e93e1b8fd 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/uninstaller/UninstallSessionFactory.kt @@ -99,16 +99,19 @@ internal class UninstallSessionFactoryImpl internal constructor( } } -private object AckpinePromptUninstallTitle : ResolvableString.Resource(R.string.ackpine_prompt_uninstall_title) { +private object AckpinePromptUninstallTitle : ResolvableString.Resource() { private const val serialVersionUID = -4086992997791586590L + override fun stringId() = R.string.ackpine_prompt_uninstall_title } -private object AckpinePromptUninstallMessage : ResolvableString.Resource(R.string.ackpine_prompt_uninstall_message) { +private object AckpinePromptUninstallMessage : ResolvableString.Resource() { private const val serialVersionUID = -3150252606151986307L + override fun stringId(): Int = R.string.ackpine_prompt_uninstall_message } -private class AckpinePromptUninstallMessageWithLabel(label: String) : - ResolvableString.Resource(R.string.ackpine_prompt_uninstall_message_with_label, label) { +private class AckpinePromptUninstallMessageWithLabel(label: String) : ResolvableString.Resource(label) { + + override fun stringId() = R.string.ackpine_prompt_uninstall_message_with_label private companion object { private const val serialVersionUID = 5259262335605612228L diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt index f463f0534..4366e7c50 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt @@ -18,11 +18,11 @@ package ru.solrudev.ackpine.session.parameters +import android.graphics.drawable.Drawable import androidx.annotation.DrawableRes import androidx.annotation.RestrictTo import ru.solrudev.ackpine.resources.ResolvableString import java.io.Serializable -import android.graphics.drawable.Drawable /** * Data for a high-priority notification which launches confirmation activity. @@ -151,14 +151,19 @@ public class NotificationData private constructor( * } * ``` */ -public abstract class DrawableId(@[DrawableRes Transient] public val drawableId: Int) : Serializable { +public abstract class DrawableId : Serializable { + + @DrawableRes + public abstract fun drawableId(): Int + private companion object { private const val serialVersionUID = 6564416758029834576L } } -private object DefaultNotificationIcon : DrawableId(android.R.drawable.ic_dialog_alert) { +private object DefaultNotificationIcon : DrawableId() { private const val serialVersionUID = 6906923061913799903L + override fun drawableId() = android.R.drawable.ic_dialog_alert } @RestrictTo(RestrictTo.Scope.LIBRARY) diff --git a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt index f64b46451..307bdee6d 100644 --- a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt +++ b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt @@ -77,11 +77,13 @@ public sealed interface ResolvableString : Serializable { * This factory is meant to create **only** transient strings, i.e. not persisted in storage. For persisted * strings [ResolvableString.Resource] should be explicitly subclassed. Example: * ``` - * object InstallMessageTitle : ResolvableString.Resource(R.string.install_message_title) { + * object InstallMessageTitle : ResolvableString.Resource() { + * override fun stringId() = R.string.install_message_title * private const val serialVersionUID = -1310602635578779088L * } * - * class InstallMessage(fileName: String) : ResolvableString.Resource(R.string.install_message, fileName) { + * class InstallMessage(fileName: String) : ResolvableString.Resource(fileName) { + * override fun stringId() = R.string.install_message * private companion object { * private const val serialVersionUID = 4749568844072243110L * } @@ -91,7 +93,9 @@ public sealed interface ResolvableString : Serializable { @Suppress("serial") @JvmStatic public fun resource(@StringRes stringId: Int, vararg args: Serializable): ResolvableString { - return object : Resource(stringId, *args) {} + return object : Resource(*args) { + override fun stringId() = stringId + } } } @@ -101,11 +105,13 @@ public sealed interface ResolvableString : Serializable { * * Should be explicitly subclassed to ensure stable persistence, and `serialVersionUID` must be present. Example: * ``` - * object InstallMessageTitle : ResolvableString.Resource(R.string.install_message_title) { + * object InstallMessageTitle : ResolvableString.Resource() { + * override fun stringId() = R.string.install_message_title * private const val serialVersionUID = -1310602635578779088L * } * - * class InstallMessage(fileName: String) : ResolvableString.Resource(R.string.install_message, fileName) { + * class InstallMessage(fileName: String) : ResolvableString.Resource(fileName) { + * override fun stringId() = R.string.install_message * private companion object { * private const val serialVersionUID = 4749568844072243110L * } @@ -113,12 +119,12 @@ public sealed interface ResolvableString : Serializable { * ``` * For transient strings, i.e. not persisted in storage, you can use [ResolvableString.resource] factory. */ - public abstract class Resource( - @[StringRes Transient] private val stringId: Int, - private vararg val args: Serializable - ) : ResolvableString { + public abstract class Resource(private vararg val args: Serializable) : ResolvableString { + + @StringRes + protected abstract fun stringId(): Int - override fun resolve(context: Context): String = context.getString(stringId, *resolveArgs(context)) + final override fun resolve(context: Context): String = context.getString(stringId(), *resolveArgs(context)) private fun resolveArgs(context: Context): Array = args.map { argument -> if (argument is ResolvableString) { @@ -132,14 +138,11 @@ public sealed interface ResolvableString : Serializable { if (this === other) return true if (javaClass != other?.javaClass) return false other as Resource - if (stringId != other.stringId) return false return args.contentEquals(other.args) } override fun hashCode(): Int { - var result = stringId - result = 31 * result + args.contentHashCode() - return result + return args.contentHashCode() } private companion object { diff --git a/docs/configuration.md b/docs/configuration.md index 17f155da1..8ac8cb779 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -27,18 +27,21 @@ An example of creating a session with custom parameters: } } - object InstallMessageTitle : ResolvableString.Resource(R.string.install_message_title) { + object InstallMessageTitle : ResolvableString.Resource() { private const val serialVersionUID = -1310602635578779088L + override fun stringId() = R.string.install_message_title } - class InstallMessage(fileName: String) : ResolvableString.Resource(R.string.install_message, fileName) { + class InstallMessage(fileName: String) : ResolvableString.Resource(fileName) { + override fun stringId() = R.string.install_message private companion object { private const val serialVersionUID = 4749568844072243110L } } - object InstallIcon : DrawableId(R.drawable.ic_install) { + object InstallIcon : DrawableId() { private const val serialVersionUID = 3692803605642002954L + override fun drawableId() = R.drawable.ic_install } ``` @@ -65,35 +68,42 @@ An example of creating a session with custom parameters: public static final DrawableId INSTALL_ICON = new InstallIcon(); private static class InstallMessageTitle extends ResolvableString.Resource { - + @Serial private static final long serialVersionUID = -1310602635578779088L; - - public InstallMessageTitle() { - super(R.string.install_message_title); + + @Override + protected int stringId() { + return R.string.install_message_title; } } public static class InstallMessage extends ResolvableString.Resource { - + @Serial private static final long serialVersionUID = 4749568844072243110L; - + public InstallMessage(String fileName) { - super(R.string.install_message, fileName); + super(fileName); + } + + @Override + protected int stringId() { + return R.string.install_message; } } private static class InstallIcon extends DrawableId { - + @Serial private static final long serialVersionUID = 3692803605642002954L; - - public InstallIcon() { - super(R.drawable.ic_install); + + @Override + public int drawableId() { + return R.drawable.ic_install; } } - + private Resources() { } } From 3accc9a7235991cab87036a57a66f872449fc46e Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 21:18:36 +0500 Subject: [PATCH 29/40] DrawableId and ResolvableString docs --- .../solrudev/ackpine/session/parameters/NotificationData.kt | 6 +++++- .../ru/solrudev/ackpine/resources/ResolvableString.kt | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt index 4366e7c50..30e6aae4f 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt @@ -146,13 +146,17 @@ public class NotificationData private constructor( * * Should be explicitly subclassed to ensure stable persistence, and `serialVersionUID` must be present. Example: * ``` - * object InstallIcon : DrawableId(R.drawable.ic_install) { + * object InstallIcon : DrawableId() { * private const val serialVersionUID = 3692803605642002954L + * override fun drawableId() = R.drawable.ic_install * } * ``` */ public abstract class DrawableId : Serializable { + /** + * Returns an Android drawable resource ID. + */ @DrawableRes public abstract fun drawableId(): Int diff --git a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt index 307bdee6d..b02e73d1c 100644 --- a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt +++ b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt @@ -121,6 +121,9 @@ public sealed interface ResolvableString : Serializable { */ public abstract class Resource(private vararg val args: Serializable) : ResolvableString { + /** + * Returns an Android string resource ID. + */ @StringRes protected abstract fun stringId(): Int From 64eeaa3211b438d77d900c31d18f122ce868628b Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 21:19:09 +0500 Subject: [PATCH 30/40] make equals() and hashCode() final in ResolvableString.Resource --- .../kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt index b02e73d1c..5a7284b8c 100644 --- a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt +++ b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt @@ -137,14 +137,14 @@ public sealed interface ResolvableString : Serializable { } }.toTypedArray() - override fun equals(other: Any?): Boolean { + final override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as Resource return args.contentEquals(other.args) } - override fun hashCode(): Int { + final override fun hashCode(): Int { return args.contentHashCode() } From 37306d6d81b60cbf90dfdd0b8dc2dd79cdc6e621 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 21:34:21 +0500 Subject: [PATCH 31/40] make DrawableId an interface --- .../ackpine/session/parameters/NotificationData.kt | 8 ++++---- docs/configuration.md | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt index 30e6aae4f..a0565121b 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationData.kt @@ -146,26 +146,26 @@ public class NotificationData private constructor( * * Should be explicitly subclassed to ensure stable persistence, and `serialVersionUID` must be present. Example: * ``` - * object InstallIcon : DrawableId() { + * object InstallIcon : DrawableId { * private const val serialVersionUID = 3692803605642002954L * override fun drawableId() = R.drawable.ic_install * } * ``` */ -public abstract class DrawableId : Serializable { +public interface DrawableId : Serializable { /** * Returns an Android drawable resource ID. */ @DrawableRes - public abstract fun drawableId(): Int + public fun drawableId(): Int private companion object { private const val serialVersionUID = 6564416758029834576L } } -private object DefaultNotificationIcon : DrawableId() { +private object DefaultNotificationIcon : DrawableId { private const val serialVersionUID = 6906923061913799903L override fun drawableId() = android.R.drawable.ic_dialog_alert } diff --git a/docs/configuration.md b/docs/configuration.md index 8ac8cb779..f047c7671 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -39,7 +39,7 @@ An example of creating a session with custom parameters: } } - object InstallIcon : DrawableId() { + object InstallIcon : DrawableId { private const val serialVersionUID = 3692803605642002954L override fun drawableId() = R.drawable.ic_install } @@ -93,7 +93,7 @@ An example of creating a session with custom parameters: } } - private static class InstallIcon extends DrawableId { + private static class InstallIcon implements DrawableId { @Serial private static final long serialVersionUID = 3692803605642002954L; From 1dda5f65e98b49a483aa89138c883b9d1e1fe422 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 21:52:37 +0500 Subject: [PATCH 32/40] reintroduce NotificationString as deprecated --- ackpine-core/consumer-rules.pro | 6 + .../session/parameters/NotificationString.kt | 223 ++++++++++++++++++ ackpine-resources/consumer-rules.pro | 2 +- 3 files changed, 230 insertions(+), 1 deletion(-) create mode 100644 ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationString.kt diff --git a/ackpine-core/consumer-rules.pro b/ackpine-core/consumer-rules.pro index 7372a1911..f7b1e6542 100644 --- a/ackpine-core/consumer-rules.pro +++ b/ackpine-core/consumer-rules.pro @@ -1,4 +1,10 @@ # Serializable +-keep class ru.solrudev.ackpine.session.parameters.NotificationString { *; } +-keep class ru.solrudev.ackpine.session.parameters.NotificationString$* { *; } +-keep class ru.solrudev.ackpine.session.parameters.Default { *; } +-keep class ru.solrudev.ackpine.session.parameters.Empty { *; } +-keep class ru.solrudev.ackpine.session.parameters.Raw { *; } +-keep class ru.solrudev.ackpine.session.parameters.Resource { *; } -keep class ru.solrudev.ackpine.installer.InstallFailure { *; } -keep class ru.solrudev.ackpine.installer.InstallFailure$* { *; } -keep class ru.solrudev.ackpine.uninstaller.UninstallFailure { *; } diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationString.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationString.kt new file mode 100644 index 000000000..c775f21c6 --- /dev/null +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/session/parameters/NotificationString.kt @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2023-2024 Ilya Fomichev + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("Unused") + +package ru.solrudev.ackpine.session.parameters + +import android.content.Context +import androidx.annotation.StringRes +import java.io.Serializable + +/** + * String for a session's [confirmation notification][NotificationData]. + */ +@Deprecated( + message = "This class cannot provide persistence stability of string resources in case of string " + + "resources updates. Use ResolvableString to provide strings to notifications. " + + "NotificationString will be removed in next minor release.", + level = DeprecationLevel.ERROR +) +public sealed interface NotificationString : Serializable { + + /** + * Returns whether this string represents a default string. + */ + @Deprecated(message = "This property is removed from ResolvableString.", level = DeprecationLevel.ERROR) + public val isDefault: Boolean + get() = this is Default + + /** + * Returns whether this string is empty. + */ + @Deprecated( + message = "This class cannot provide persistence stability of string resources in case of string " + + "resources updates. Use ResolvableString to provide strings to notifications. " + + "NotificationString will be removed in next minor release.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith(expression = "ResolvableString.isEmpty") + ) + public val isEmpty: Boolean + get() = this is Empty + + /** + * Returns whether this string represents a hardcoded string. + */ + @Deprecated( + message = "This class cannot provide persistence stability of string resources in case of string " + + "resources updates. Use ResolvableString to provide strings to notifications. " + + "NotificationString will be removed in next minor release.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith(expression = "ResolvableString.isRaw") + ) + public val isRaw: Boolean + get() = this is Raw + + /** + * Returns whether this string represents a resource string. + */ + @Deprecated( + message = "This class cannot provide persistence stability of string resources in case of string " + + "resources updates. Use ResolvableString to provide strings to notifications. " + + "NotificationString will be removed in next minor release.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith(expression = "ResolvableString.isResource") + ) + public val isResource: Boolean + get() = this is Resource + + /** + * Resolves string value for a given [context]. + */ + public fun resolve(context: Context): String + + @Deprecated( + message = "This class cannot provide persistence stability of string resources in case of string " + + "resources updates. Use ResolvableString to provide strings to notifications. " + + "NotificationString will be removed in next minor release.", + level = DeprecationLevel.ERROR + ) + public companion object { + + /** + * Creates a default [NotificationString]. + */ + @JvmStatic + @Suppress("DEPRECATION_ERROR") + @Deprecated( + message = "This class cannot provide persistence stability of string resources in case of string " + + "resources updates. Use ResolvableString to provide strings to notifications. " + + "NotificationString will be removed in next minor release.", + level = DeprecationLevel.ERROR + ) + public fun default(): NotificationString = Default + + /** + * Creates an empty [NotificationString]. + */ + @JvmStatic + @Suppress("DEPRECATION_ERROR") + @Deprecated( + message = "This class cannot provide persistence stability of string resources in case of string " + + "resources updates. Use ResolvableString to provide strings to notifications. " + + "NotificationString will be removed in next minor release.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith( + expression = "ResolvableString.empty()", + imports = ["ru.solrudev.ackpine.resources.ResolvableString"] + ) + ) + public fun empty(): NotificationString = Empty + + /** + * Creates [NotificationString] with a hardcoded value. + */ + @JvmStatic + @Suppress("DEPRECATION_ERROR") + @Deprecated( + message = "This class cannot provide persistence stability of string resources in case of string " + + "resources updates. Use ResolvableString to provide strings to notifications. " + + "NotificationString will be removed in next minor release.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith( + expression = "ResolvableString.raw(value)", + imports = ["ru.solrudev.ackpine.resources.ResolvableString"] + ) + ) + public fun raw(value: String): NotificationString { + if (value.isEmpty()) { + return Empty + } + return Raw(value) + } + + /** + * Creates [NotificationString] represented by Android resource string with optional arguments. Arguments can be + * [NotificationStrings][NotificationString] as well. + */ + @JvmStatic + @Suppress("DEPRECATION_ERROR") + @Deprecated( + message = "This class cannot provide persistence stability of string resources in case of string " + + "resources updates. Subclass ResolvableString.Resource to provide strings to notifications. " + + "NotificationString will be removed in next minor release.", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith( + expression = "class StringResource(vararg args: Serializable) : ResolvableString.Resource(*args) { \n" + + "\toverride fun stringId() = stringId\n\tprivate companion object {\n" + + "\t\tprivate const val serialVersionUID = PUT_STRING_RESOURCE_SERIAL_VERSION_UID_HERE\n" + + "\t}\n}", + imports = ["ru.solrudev.ackpine.resources.ResolvableString"] + ) + ) + public fun resource(@StringRes stringId: Int, vararg args: Serializable): NotificationString { + return Resource(stringId, args) + } + } +} + +@Suppress("DEPRECATION_ERROR") +private data object Default : NotificationString { + private const val serialVersionUID = 809543744617543082L + override fun resolve(context: Context): String = "" +} + +@Suppress("DEPRECATION_ERROR") +private data object Empty : NotificationString { + private const val serialVersionUID: Long = 5194188194930148316L + override fun resolve(context: Context): String = "" +} + +@Suppress("DEPRECATION_ERROR") +private data class Raw(val value: String) : NotificationString { + override fun resolve(context: Context): String = value + + private companion object { + private const val serialVersionUID: Long = -6824736411987160679L + } +} + +@Suppress("DEPRECATION_ERROR") +private data class Resource(@StringRes val stringId: Int, val args: Array) : NotificationString { + + override fun resolve(context: Context): String = context.getString(stringId, *resolveArgs(context)) + + private fun resolveArgs(context: Context): Array = args.map { argument -> + if (argument is NotificationString) { + argument.resolve(context) + } else { + argument + } + }.toTypedArray() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Resource + if (stringId != other.stringId) return false + return args.contentEquals(other.args) + } + + override fun hashCode(): Int { + var result = stringId + result = 31 * result + args.contentHashCode() + return result + } + + private companion object { + private const val serialVersionUID: Long = -7822872422889864805L + } +} \ No newline at end of file diff --git a/ackpine-resources/consumer-rules.pro b/ackpine-resources/consumer-rules.pro index c2d5fee37..b6eca166f 100644 --- a/ackpine-resources/consumer-rules.pro +++ b/ackpine-resources/consumer-rules.pro @@ -1,6 +1,6 @@ # Serializable -keep class ru.solrudev.ackpine.resources.ResolvableString { *; } -keep class ru.solrudev.ackpine.resources.ResolvableString$* { *; } --keep class * extends ru.solrudev.ackpine.resources.ResolvableString$Resource +-keep class * extends ru.solrudev.ackpine.resources.ResolvableString$Resource { *; } -keep class ru.solrudev.ackpine.resources.Empty { *; } -keep class ru.solrudev.ackpine.resources.Raw { *; } \ No newline at end of file From d82bbfe37a53a87645eac4a9f8c50fe19fc22221 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 22:06:18 +0500 Subject: [PATCH 33/40] add keep rule for DrawableId --- ackpine-core/consumer-rules.pro | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ackpine-core/consumer-rules.pro b/ackpine-core/consumer-rules.pro index f7b1e6542..e0bbe619f 100644 --- a/ackpine-core/consumer-rules.pro +++ b/ackpine-core/consumer-rules.pro @@ -5,6 +5,8 @@ -keep class ru.solrudev.ackpine.session.parameters.Empty { *; } -keep class ru.solrudev.ackpine.session.parameters.Raw { *; } -keep class ru.solrudev.ackpine.session.parameters.Resource { *; } +-keep interface ru.solrudev.ackpine.session.parameters.DrawableId { *; } +-keep class * implements ru.solrudev.ackpine.session.parameters.DrawableId { *; } -keep class ru.solrudev.ackpine.installer.InstallFailure { *; } -keep class ru.solrudev.ackpine.installer.InstallFailure$* { *; } -keep class ru.solrudev.ackpine.uninstaller.UninstallFailure { *; } From 53797ef6dd7cd138ec0c48d1c4cdb31712f4d043 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 22:10:13 +0500 Subject: [PATCH 34/40] ResolvableString docs --- .../kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt index 5a7284b8c..60c1bb936 100644 --- a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt +++ b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt @@ -70,7 +70,7 @@ public sealed interface ResolvableString : Serializable { } /** - * Creates an anonymous instance of [ResolvableString.Resource], which is a [ResolvableString] represented by + * Creates an anonymous instance of [ResolvableString.Resource], which is a [ResolvableString] backed by * Android resource string with optional arguments. Arguments can be [ResolvableStrings][ResolvableString] * as well. * @@ -100,7 +100,7 @@ public sealed interface ResolvableString : Serializable { } /** - * [ResolvableString] represented by Android resource string with optional arguments. Arguments can be + * [ResolvableString] backed by Android resource string with optional arguments. Arguments can be * [ResolvableStrings][ResolvableString] as well. * * Should be explicitly subclassed to ensure stable persistence, and `serialVersionUID` must be present. Example: From f95a8ed94624e40b23ed5fbacfba03f0fbd1a678 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Thu, 24 Oct 2024 23:38:52 +0500 Subject: [PATCH 35/40] rename ResolvableString.resource() to ResolvableString.transientResource() --- .../solrudev/ackpine/resources/ResolvableString.kt | 6 +++--- .../ackpine/sample/install/InstallViewModel.java | 14 +++++++------- .../ackpine/sample/install/InstallViewModel.kt | 14 +++++++------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt index 60c1bb936..5acdefc9c 100644 --- a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt +++ b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt @@ -74,7 +74,7 @@ public sealed interface ResolvableString : Serializable { * Android resource string with optional arguments. Arguments can be [ResolvableStrings][ResolvableString] * as well. * - * This factory is meant to create **only** transient strings, i.e. not persisted in storage. For persisted + * This factory is meant to create only **transient** strings, i.e. not persisted in storage. For persisted * strings [ResolvableString.Resource] should be explicitly subclassed. Example: * ``` * object InstallMessageTitle : ResolvableString.Resource() { @@ -92,7 +92,7 @@ public sealed interface ResolvableString : Serializable { */ @Suppress("serial") @JvmStatic - public fun resource(@StringRes stringId: Int, vararg args: Serializable): ResolvableString { + public fun transientResource(@StringRes stringId: Int, vararg args: Serializable): ResolvableString { return object : Resource(*args) { override fun stringId() = stringId } @@ -117,7 +117,7 @@ public sealed interface ResolvableString : Serializable { * } * } * ``` - * For transient strings, i.e. not persisted in storage, you can use [ResolvableString.resource] factory. + * For transient strings, i.e. not persisted in storage, you can use [ResolvableString.transientResource] factory. */ public abstract class Resource(private vararg val args: Serializable) : ResolvableString { diff --git a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java index 9a70e53ce..59f2af3ba 100644 --- a/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java +++ b/sample-java/src/main/java/ru/solrudev/ackpine/sample/install/InstallViewModel.java @@ -187,16 +187,16 @@ private List mapApkSequenceToUri(@NonNull Sequence apks) { return uris; } catch (SplitPackageException exception) { if (exception instanceof NoBaseApkException) { - error.postValue(ResolvableString.resource(R.string.error_no_base_apk)); + error.postValue(ResolvableString.transientResource(R.string.error_no_base_apk)); } else if (exception instanceof ConflictingBaseApkException) { - error.postValue(ResolvableString.resource(R.string.error_conflicting_base_apk)); + error.postValue(ResolvableString.transientResource(R.string.error_conflicting_base_apk)); } else if (exception instanceof ConflictingSplitNameException e) { - error.postValue(ResolvableString.resource(R.string.error_conflicting_split_name, e.getName())); + error.postValue(ResolvableString.transientResource(R.string.error_conflicting_split_name, e.getName())); } else if (exception instanceof ConflictingPackageNameException e) { - error.postValue(ResolvableString.resource(R.string.error_conflicting_package_name, + error.postValue(ResolvableString.transientResource(R.string.error_conflicting_package_name, e.getExpected(), e.getActual(), e.getName())); } else if (exception instanceof ConflictingVersionCodeException e) { - error.postValue(ResolvableString.resource(R.string.error_conflicting_version_code, + error.postValue(ResolvableString.transientResource(R.string.error_conflicting_version_code, e.getExpected(), e.getActual(), e.getName())); } return Collections.emptyList(); @@ -228,8 +228,8 @@ public void onSuccess(@NonNull UUID sessionId) { public void onFailure(@NonNull UUID sessionId, @NonNull InstallFailure failure) { final var message = failure.getMessage(); final var error = message != null - ? ResolvableString.resource(R.string.session_error_with_reason, message) - : ResolvableString.resource(R.string.session_error); + ? ResolvableString.transientResource(R.string.session_error_with_reason, message) + : ResolvableString.transientResource(R.string.session_error); sessionDataRepository.setError(sessionId, error); if (failure instanceof Failure.Exceptional f) { Log.e("InstallViewModel", null, f.getException()); diff --git a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt index 0b63a3a9c..a1a2b1b88 100644 --- a/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt +++ b/sample-ktx/src/main/kotlin/ru/solrudev/ackpine/sample/install/InstallViewModel.kt @@ -136,9 +136,9 @@ class InstallViewModel( private fun handleSessionError(message: String?, sessionId: UUID) { val error = if (message != null) { - ResolvableString.resource(R.string.session_error_with_reason, message) + ResolvableString.transientResource(R.string.session_error_with_reason, message) } else { - ResolvableString.resource(R.string.session_error) + ResolvableString.transientResource(R.string.session_error) } sessionDataRepository.setError(sessionId, error) } @@ -148,19 +148,19 @@ class InstallViewModel( return map { it.uri }.toList() } catch (exception: SplitPackageException) { val errorString = when (exception) { - is NoBaseApkException -> ResolvableString.resource(R.string.error_no_base_apk) - is ConflictingBaseApkException -> ResolvableString.resource(R.string.error_conflicting_base_apk) - is ConflictingSplitNameException -> ResolvableString.resource( + is NoBaseApkException -> ResolvableString.transientResource(R.string.error_no_base_apk) + is ConflictingBaseApkException -> ResolvableString.transientResource(R.string.error_conflicting_base_apk) + is ConflictingSplitNameException -> ResolvableString.transientResource( R.string.error_conflicting_split_name, exception.name ) - is ConflictingPackageNameException -> ResolvableString.resource( + is ConflictingPackageNameException -> ResolvableString.transientResource( R.string.error_conflicting_package_name, exception.expected, exception.actual, exception.name ) - is ConflictingVersionCodeException -> ResolvableString.resource( + is ConflictingVersionCodeException -> ResolvableString.transientResource( R.string.error_conflicting_version_code, exception.expected, exception.actual, exception.name ) From d1fc0881bb9009f04110436e95218856e7fddc5a Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 25 Oct 2024 02:10:56 +0500 Subject: [PATCH 36/40] dump api --- ackpine-core/api/ackpine-core.api | 24 +++++++++++--------- ackpine-ktx/api/ackpine-ktx.api | 24 ++++++++++---------- ackpine-resources/api/ackpine-resources.api | 25 +++++++++++++++++++++ 3 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 ackpine-resources/api/ackpine-resources.api diff --git a/ackpine-core/api/ackpine-core.api b/ackpine-core/api/ackpine-core.api index 050cc98f3..e2ee69cd5 100644 --- a/ackpine-core/api/ackpine-core.api +++ b/ackpine-core/api/ackpine-core.api @@ -458,14 +458,18 @@ public abstract interface class ru/solrudev/ackpine/session/parameters/Confirmat public abstract fun getNotificationData ()Lru/solrudev/ackpine/session/parameters/NotificationData; } +public abstract interface class ru/solrudev/ackpine/session/parameters/DrawableId : java/io/Serializable { + public abstract fun drawableId ()I +} + public final class ru/solrudev/ackpine/session/parameters/NotificationData { public static final field Companion Lru/solrudev/ackpine/session/parameters/NotificationData$Companion; public static final field DEFAULT Lru/solrudev/ackpine/session/parameters/NotificationData; - public synthetic fun (ILru/solrudev/ackpine/session/parameters/NotificationString;Lru/solrudev/ackpine/session/parameters/NotificationString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Lru/solrudev/ackpine/session/parameters/DrawableId;Lru/solrudev/ackpine/resources/ResolvableString;Lru/solrudev/ackpine/resources/ResolvableString;Lkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z - public final fun getContentText ()Lru/solrudev/ackpine/session/parameters/NotificationString; - public final fun getIcon ()I - public final fun getTitle ()Lru/solrudev/ackpine/session/parameters/NotificationString; + public final fun getContentText ()Lru/solrudev/ackpine/resources/ResolvableString; + public final fun getIcon ()Lru/solrudev/ackpine/session/parameters/DrawableId; + public final fun getTitle ()Lru/solrudev/ackpine/resources/ResolvableString; public fun hashCode ()I public fun toString ()Ljava/lang/String; } @@ -473,12 +477,12 @@ public final class ru/solrudev/ackpine/session/parameters/NotificationData { public final class ru/solrudev/ackpine/session/parameters/NotificationData$Builder { public fun ()V public final fun build ()Lru/solrudev/ackpine/session/parameters/NotificationData; - public final fun getContentText ()Lru/solrudev/ackpine/session/parameters/NotificationString; - public final fun getIcon ()I - public final fun getTitle ()Lru/solrudev/ackpine/session/parameters/NotificationString; - public final fun setContentText (Lru/solrudev/ackpine/session/parameters/NotificationString;)Lru/solrudev/ackpine/session/parameters/NotificationData$Builder; - public final fun setIcon (I)Lru/solrudev/ackpine/session/parameters/NotificationData$Builder; - public final fun setTitle (Lru/solrudev/ackpine/session/parameters/NotificationString;)Lru/solrudev/ackpine/session/parameters/NotificationData$Builder; + public final fun getContentText ()Lru/solrudev/ackpine/resources/ResolvableString; + public final fun getIcon ()Lru/solrudev/ackpine/session/parameters/DrawableId; + public final fun getTitle ()Lru/solrudev/ackpine/resources/ResolvableString; + public final fun setContentText (Lru/solrudev/ackpine/resources/ResolvableString;)Lru/solrudev/ackpine/session/parameters/NotificationData$Builder; + public final fun setIcon (Lru/solrudev/ackpine/session/parameters/DrawableId;)Lru/solrudev/ackpine/session/parameters/NotificationData$Builder; + public final fun setTitle (Lru/solrudev/ackpine/resources/ResolvableString;)Lru/solrudev/ackpine/session/parameters/NotificationData$Builder; } public final class ru/solrudev/ackpine/session/parameters/NotificationData$Companion { diff --git a/ackpine-ktx/api/ackpine-ktx.api b/ackpine-ktx/api/ackpine-ktx.api index 6fa53486d..28a8c148c 100644 --- a/ackpine-ktx/api/ackpine-ktx.api +++ b/ackpine-ktx/api/ackpine-ktx.api @@ -98,23 +98,23 @@ public final class ru/solrudev/ackpine/session/parameters/ConfirmationDslKt { } public abstract interface class ru/solrudev/ackpine/session/parameters/NotificationDataDsl { - public abstract fun getContentText ()Lru/solrudev/ackpine/session/parameters/NotificationString; - public abstract fun getIcon ()I - public abstract fun getTitle ()Lru/solrudev/ackpine/session/parameters/NotificationString; - public abstract fun setContentText (Lru/solrudev/ackpine/session/parameters/NotificationString;)V - public abstract fun setIcon (I)V - public abstract fun setTitle (Lru/solrudev/ackpine/session/parameters/NotificationString;)V + public abstract fun getContentText ()Lru/solrudev/ackpine/resources/ResolvableString; + public abstract fun getIcon ()Lru/solrudev/ackpine/session/parameters/DrawableId; + public abstract fun getTitle ()Lru/solrudev/ackpine/resources/ResolvableString; + public abstract fun setContentText (Lru/solrudev/ackpine/resources/ResolvableString;)V + public abstract fun setIcon (Lru/solrudev/ackpine/session/parameters/DrawableId;)V + public abstract fun setTitle (Lru/solrudev/ackpine/resources/ResolvableString;)V } public final class ru/solrudev/ackpine/session/parameters/NotificationDataDslBuilder : ru/solrudev/ackpine/session/parameters/NotificationDataDsl { public fun ()V public final fun build ()Lru/solrudev/ackpine/session/parameters/NotificationData; - public fun getContentText ()Lru/solrudev/ackpine/session/parameters/NotificationString; - public fun getIcon ()I - public fun getTitle ()Lru/solrudev/ackpine/session/parameters/NotificationString; - public fun setContentText (Lru/solrudev/ackpine/session/parameters/NotificationString;)V - public fun setIcon (I)V - public fun setTitle (Lru/solrudev/ackpine/session/parameters/NotificationString;)V + public fun getContentText ()Lru/solrudev/ackpine/resources/ResolvableString; + public fun getIcon ()Lru/solrudev/ackpine/session/parameters/DrawableId; + public fun getTitle ()Lru/solrudev/ackpine/resources/ResolvableString; + public fun setContentText (Lru/solrudev/ackpine/resources/ResolvableString;)V + public fun setIcon (Lru/solrudev/ackpine/session/parameters/DrawableId;)V + public fun setTitle (Lru/solrudev/ackpine/resources/ResolvableString;)V } public final class ru/solrudev/ackpine/session/parameters/NotificationDataKt { diff --git a/ackpine-resources/api/ackpine-resources.api b/ackpine-resources/api/ackpine-resources.api new file mode 100644 index 000000000..47b092833 --- /dev/null +++ b/ackpine-resources/api/ackpine-resources.api @@ -0,0 +1,25 @@ +public abstract interface class ru/solrudev/ackpine/resources/ResolvableString : java/io/Serializable { + public static final field Companion Lru/solrudev/ackpine/resources/ResolvableString$Companion; + public static fun empty ()Lru/solrudev/ackpine/resources/ResolvableString; + public fun isEmpty ()Z + public fun isRaw ()Z + public fun isResource ()Z + public static fun raw (Ljava/lang/String;)Lru/solrudev/ackpine/resources/ResolvableString; + public abstract fun resolve (Landroid/content/Context;)Ljava/lang/String; + public static fun transientResource (I[Ljava/io/Serializable;)Lru/solrudev/ackpine/resources/ResolvableString; +} + +public final class ru/solrudev/ackpine/resources/ResolvableString$Companion { + public final fun empty ()Lru/solrudev/ackpine/resources/ResolvableString; + public final fun raw (Ljava/lang/String;)Lru/solrudev/ackpine/resources/ResolvableString; + public final fun transientResource (I[Ljava/io/Serializable;)Lru/solrudev/ackpine/resources/ResolvableString; +} + +public abstract class ru/solrudev/ackpine/resources/ResolvableString$Resource : ru/solrudev/ackpine/resources/ResolvableString { + public fun ([Ljava/io/Serializable;)V + public final fun equals (Ljava/lang/Object;)Z + public final fun hashCode ()I + public final fun resolve (Landroid/content/Context;)Ljava/lang/String; + protected abstract fun stringId ()I +} + From c8e484a5e1bd76078695f0a89d3645f468d56953 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 25 Oct 2024 02:23:30 +0500 Subject: [PATCH 37/40] improve ResolvableString docs --- .../ru/solrudev/ackpine/resources/ResolvableString.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt index 5acdefc9c..6dd932b4e 100644 --- a/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt +++ b/ackpine-resources/src/main/kotlin/ru/solrudev/ackpine/resources/ResolvableString.kt @@ -71,8 +71,8 @@ public sealed interface ResolvableString : Serializable { /** * Creates an anonymous instance of [ResolvableString.Resource], which is a [ResolvableString] backed by - * Android resource string with optional arguments. Arguments can be [ResolvableStrings][ResolvableString] - * as well. + * Android resource string with optional [arguments][args]. Arguments can be + * [ResolvableStrings][ResolvableString] as well. * * This factory is meant to create only **transient** strings, i.e. not persisted in storage. For persisted * strings [ResolvableString.Resource] should be explicitly subclassed. Example: @@ -89,6 +89,9 @@ public sealed interface ResolvableString : Serializable { * } * } * ``` + * + * @param stringId Android string resource ID + * @param args string format arguments */ @Suppress("serial") @JvmStatic @@ -100,7 +103,7 @@ public sealed interface ResolvableString : Serializable { } /** - * [ResolvableString] backed by Android resource string with optional arguments. Arguments can be + * [ResolvableString] backed by Android resource string with optional [arguments][args]. Arguments can be * [ResolvableStrings][ResolvableString] as well. * * Should be explicitly subclassed to ensure stable persistence, and `serialVersionUID` must be present. Example: @@ -118,6 +121,8 @@ public sealed interface ResolvableString : Serializable { * } * ``` * For transient strings, i.e. not persisted in storage, you can use [ResolvableString.transientResource] factory. + * + * @param args string format arguments */ public abstract class Resource(private vararg val args: Serializable) : ResolvableString { From 28d1c4fa7a3d74ccfa7f671b8d971bd7d48de76a Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 25 Oct 2024 02:30:31 +0500 Subject: [PATCH 38/40] add version 0.8.0 to changelog --- docs/changelog.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/docs/changelog.md b/docs/changelog.md index f1c014d83..36ab7609e 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,6 +1,47 @@ Change Log ========== +Version 0.8.0 (2024-10-25) +-------------------------- + +### Dependencies + +- Extracted `ackpine-resources` artifact, which is now depended upon by `ackpine-core`. + +### Bug fixes and improvements + +- `NotificationString` is superseded by `ResolvableString` to accommodate stable string resources resolution. `ResolvableString` is now located in `ackpine-resources` artifact and can also be used separately for general app needs. `NotificationString` is deprecated and will be removed in next minor release. + + To migrate `NotificationString.resource()` usages to `ResolvableString`, create classes inheriting from `ResolvableString.Resource` like this: + ```kotlin + // Old + NotificationString.resource(R.string.install_message, fileName) + + // New + class InstallMessage(fileName: String) : ResolvableString.Resource(fileName) { + override fun stringId() = R.string.install_message + private companion object { + private const val serialVersionUID = 4749568844072243110L + } + } + + InstallMessage(fileName) + ``` + + Note that this requires to purge internal database because of incompatible changes, so all previous sessions will be cleared when Ackpine is updated to 0.8.0. + +- `NotificationData` now requires an instance of `DrawableId` class instead of integer drawable resource ID for icon to accommodate stable drawable resources resolution. +- Don't hardcode a condition in implementation of `SESSION_BASED` sessions when Android's `PackageInstaller.Session` fails without report. It should possibly improve reliability on different devices. +- Fix progress bars on install screen not using latest value in sample apps. +- Disable cancel button when session's state is Committed in sample apps. + +### Public API changes + +- Breaking: `NotificationData`, `NotificationData.Builder` and `NotificationDataDsl` now require `ResolvableString` instead of `NotificationString` as `title` and `contentText` type. `NotificationString` is deprecated with an error deprecation level and will be removed in next minor release. +- Breaking: `NotificationData`, `NotificationData.Builder` and `NotificationDataDsl` now require `DrawableId` instead of integer as `icon` type. +- Added `ResolvableString` sealed interface in `ackpine-resources` module. +- Added `DrawableId` interface in `ackpine-core` module. + Version 0.7.6 (2024-10-12) -------------------------- From 79324ea8344152e91b70c10978b779501cd6f033 Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 25 Oct 2024 02:40:59 +0500 Subject: [PATCH 39/40] configuration docs --- docs/configuration.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index f047c7671..57edb7711 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -62,7 +62,7 @@ An example of creating a session with custom parameters: .build()) .build()); - public class Resources { + public abstract class Resources { public static final ResolvableString INSTALL_MESSAGE_TITLE = new InstallMessageTitle(); public static final DrawableId INSTALL_ICON = new InstallIcon(); @@ -103,9 +103,6 @@ An example of creating a session with custom parameters: return R.drawable.ic_install; } } - - private Resources() { - } } ``` From 971e000f4f7aabd3dace7c5e58b39613229bc1ab Mon Sep 17 00:00:00 2001 From: Ilya Fomichev Date: Fri, 25 Oct 2024 18:02:15 +0500 Subject: [PATCH 40/40] fix comment in SessionBasedInstallConfirmationActivity --- .../impl/installer/activity/SessionBasedInstallActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/activity/SessionBasedInstallActivity.kt b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/activity/SessionBasedInstallActivity.kt index 3d97c7c3a..270b248d7 100644 --- a/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/activity/SessionBasedInstallActivity.kt +++ b/ackpine-core/src/main/kotlin/ru/solrudev/ackpine/impl/installer/activity/SessionBasedInstallActivity.kt @@ -83,7 +83,7 @@ internal class SessionBasedInstallConfirmationActivity : InstallActivity(CONFIRM return } val sessionInfo = packageInstaller.getSessionInfo(sessionId) - // Hacky workaround: progress not going higher than 0.8 means session is dead. This is needed to complete + // Hacky workaround: progress not going higher after commit means session is dead. This is needed to complete // the Ackpine session with failure on reasons which are not handled in PackageInstallerStatusReceiver. // For example, "There was a problem parsing the package" error falls under that. val isSessionAlive = sessionInfo != null && sessionInfo.progress >= getSessionBasedSessionCommitProgressValue()