Skip to content

Commit

Permalink
Merge pull request #96 from solrudev/develop
Browse files Browse the repository at this point in the history
0.9.1
  • Loading branch information
solrudev authored Dec 14, 2024
2 parents d481630 + 224409a commit 02adf26
Show file tree
Hide file tree
Showing 30 changed files with 575 additions and 344 deletions.
2 changes: 1 addition & 1 deletion .github/ci-gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1g
org.gradle.daemon=false
org.gradle.caching=true
org.gradle.parallel=true
Expand Down
21 changes: 18 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Copy gradle.properties
run: |
Expand Down Expand Up @@ -51,6 +53,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Copy gradle.properties
run: |
mkdir -p ~/.gradle
cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Set up JDK
uses: actions/setup-java@v4
Expand Down Expand Up @@ -83,6 +92,12 @@ jobs:
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false

- name: Copy gradle.properties
run: |
mkdir -p ~/.gradle
cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Set up JDK
uses: actions/setup-java@v4
Expand All @@ -109,6 +124,6 @@ jobs:
if: success()
uses: JamesIves/github-pages-deploy-action@v4
with:
BRANCH: gh-pages
FOLDER: site
SINGLE_COMMIT: true
branch: gh-pages
folder: site
single-commit: true
9 changes: 9 additions & 0 deletions .github/workflows/github-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Copy gradle.properties
run: |
Expand Down Expand Up @@ -51,6 +53,13 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Copy gradle.properties
run: |
mkdir -p ~/.gradle
cp .github/ci-gradle.properties ~/.gradle/gradle.properties
- name: Set up JDK
uses: actions/setup-java@v4
Expand Down
6 changes: 2 additions & 4 deletions .github/workflows/release-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,18 @@ jobs:
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')

permissions:
contents: write

steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: '0'
persist-credentials: false

- name: Push tag
uses: anothrNick/github-tag-action@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.PAT_AUTOTAG }}
CUSTOM_TAG: ${{ github.event.pull_request.title }}
WITH_V: false
PRERELEASE: false
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Ackpine depends on Jetpack libraries, so it's necessary to declare the `google()

```kotlin
dependencies {
val ackpineVersion = "0.9.0"
val ackpineVersion = "0.9.1"
implementation("ru.solrudev.ackpine:ackpine-core:$ackpineVersion")

// optional - Kotlin extensions and Coroutines support
Expand Down
1 change: 0 additions & 1 deletion ackpine-core/api/ackpine-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ public final class ru/solrudev/ackpine/impl/database/dao/LastUpdateTimestampDao_

public final class ru/solrudev/ackpine/impl/database/dao/NativeSessionIdDao_Impl : ru/solrudev/ackpine/impl/database/dao/NativeSessionIdDao {
public fun <init> (Landroidx/room/RoomDatabase;)V
public fun getNativeSessionId (Ljava/lang/String;)Ljava/lang/Integer;
public static fun getRequiredConverters ()Ljava/util/List;
public fun setNativeSessionId (Ljava/lang/String;I)V
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ import androidx.room.Query
@Dao
internal interface NativeSessionIdDao {

@Query("SELECT native_session_id FROM sessions_native_session_ids WHERE session_id = :sessionId")
fun getNativeSessionId(sessionId: String): Int?

@Query("INSERT OR REPLACE INTO sessions_native_session_ids(session_id, native_session_id) " +
"VALUES (:sessionId, :nativeSessionId)")
fun setNativeSessionId(sessionId: String, nativeSessionId: Int)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,14 @@ internal data class SessionEntity internal constructor(
entity = PackageSourceEntity::class,
projection = ["package_source"]
)
val packageSource: PackageSource?
val packageSource: PackageSource?,
@Relation(
parentColumn = "id",
entityColumn = "session_id",
entity = NativeSessionIdEntity::class,
projection = ["native_session_id"]
)
val nativeSessionId: Int? = -1
)

@RestrictTo(RestrictTo.Scope.LIBRARY)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,33 @@ import android.annotation.SuppressLint
import android.content.Context
import android.os.Handler
import androidx.annotation.RestrictTo
import androidx.annotation.WorkerThread
import androidx.core.net.toUri
import ru.solrudev.ackpine.core.R
import ru.solrudev.ackpine.exceptions.SplitPackagesNotSupportedException
import ru.solrudev.ackpine.helpers.concurrent.BinarySemaphore
import ru.solrudev.ackpine.impl.database.dao.InstallConstraintsDao
import ru.solrudev.ackpine.impl.database.dao.InstallPreapprovalDao
import ru.solrudev.ackpine.impl.database.dao.InstallSessionDao
import ru.solrudev.ackpine.impl.database.dao.LastUpdateTimestampDao
import ru.solrudev.ackpine.impl.database.dao.NativeSessionIdDao
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.InstallSessionFactory.AdditionalParameters
import ru.solrudev.ackpine.impl.database.model.SessionEntity
import ru.solrudev.ackpine.impl.installer.session.IntentBasedInstallSession
import ru.solrudev.ackpine.impl.installer.session.SessionBasedInstallSession
import ru.solrudev.ackpine.impl.installer.session.getSessionBasedSessionCommitProgressValue
import ru.solrudev.ackpine.impl.installer.session.helpers.PROGRESS_MAX
import ru.solrudev.ackpine.installer.InstallFailure
import ru.solrudev.ackpine.installer.parameters.InstallParameters
import ru.solrudev.ackpine.installer.parameters.InstallerType
import ru.solrudev.ackpine.installer.parameters.PackageSource
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.Session.State.Committed
import ru.solrudev.ackpine.session.Session.State.Succeeded
import ru.solrudev.ackpine.session.parameters.DEFAULT_NOTIFICATION_STRING
import ru.solrudev.ackpine.session.parameters.NotificationData
import java.util.UUID
Expand All @@ -53,31 +60,26 @@ internal interface InstallSessionFactory {
fun create(
parameters: InstallParameters,
id: UUID,
initialState: Session.State<InstallFailure>,
initialProgress: Progress,
notificationId: Int,
dbWriteSemaphore: BinarySemaphore,
additionalParameters: AdditionalParameters = AdditionalParameters()
dbWriteSemaphore: BinarySemaphore
): ProgressSession<InstallFailure>

fun resolveNotificationData(notificationData: NotificationData, name: String): NotificationData
@WorkerThread
fun create(
session: SessionEntity.InstallSession,
needToCompleteIfSucceeded: Boolean = false
): ProgressSession<InstallFailure>

@RestrictTo(RestrictTo.Scope.LIBRARY)
data class AdditionalParameters(
val packageName: String = "",
val lastUpdateTimestamp: Long = Long.MAX_VALUE,
val needToCompleteIfSucceeded: Boolean = false,
val commitAttemptsCount: Int = 0,
val isPreapproved: Boolean = false
)
fun resolveNotificationData(notificationData: NotificationData, name: String): NotificationData
}

@SuppressLint("NewApi")
@RestrictTo(RestrictTo.Scope.LIBRARY)
internal class InstallSessionFactoryImpl internal constructor(
private val applicationContext: Context,
private val lastUpdateTimestampDao: LastUpdateTimestampDao,
private val installSessionDao: InstallSessionDao,
private val sessionDao: SessionDao,
private val sessionFailureDao: SessionFailureDao<InstallFailure>,
private val sessionProgressDao: SessionProgressDao,
private val nativeSessionIdDao: NativeSessionIdDao,
private val installPreapprovalDao: InstallPreapprovalDao,
Expand All @@ -86,47 +88,55 @@ internal class InstallSessionFactoryImpl internal constructor(
private val handler: Handler
) : InstallSessionFactory {

@SuppressLint("NewApi")
override fun create(
parameters: InstallParameters,
id: UUID,
initialState: Session.State<InstallFailure>,
initialProgress: Progress,
notificationId: Int,
dbWriteSemaphore: BinarySemaphore,
additionalParameters: AdditionalParameters
dbWriteSemaphore: BinarySemaphore
): ProgressSession<InstallFailure> = when (parameters.installerType) {
InstallerType.INTENT_BASED -> IntentBasedInstallSession(
applicationContext,
apk = parameters.apks.toList().singleOrNull() ?: throw SplitPackagesNotSupportedException(),
id, initialState, initialProgress,
id,
initialState = Session.State.Pending,
initialProgress = Progress(),
parameters.confirmation,
resolveNotificationData(parameters.notificationData, parameters.name),
lastUpdateTimestampDao, sessionDao, sessionFailureDao, sessionProgressDao,
executor, handler,
notificationId,
additionalParameters.packageName,
additionalParameters.lastUpdateTimestamp,
additionalParameters.needToCompleteIfSucceeded,
dbWriteSemaphore
lastUpdateTimestampDao, sessionDao,
sessionFailureDao = installSessionDao,
sessionProgressDao, executor, handler, notificationId, dbWriteSemaphore
)

InstallerType.SESSION_BASED -> SessionBasedInstallSession(
applicationContext,
apks = parameters.apks.toList(),
id, initialState, initialProgress,
id,
initialState = Session.State.Pending,
initialProgress = Progress(),
parameters.confirmation,
resolveNotificationData(parameters.notificationData, parameters.name),
parameters.requireUserAction,
parameters.installMode, parameters.preapproval, parameters.constraints,
parameters.requireUserAction, parameters.installMode, parameters.preapproval, parameters.constraints,
parameters.requestUpdateOwnership, parameters.packageSource,
sessionDao, sessionFailureDao, sessionProgressDao, nativeSessionIdDao, installPreapprovalDao,
installConstraintsDao, executor, handler, notificationId,
additionalParameters.commitAttemptsCount, additionalParameters.isPreapproved,
sessionDao,
sessionFailureDao = installSessionDao,
sessionProgressDao, nativeSessionIdDao, installPreapprovalDao, installConstraintsDao,
executor, handler,
nativeSessionId = -1,
notificationId,
commitAttemptsCount = 0,
isPreapproved = false,
dbWriteSemaphore
)
}

override fun create(
session: SessionEntity.InstallSession,
needToCompleteIfSucceeded: Boolean
): ProgressSession<InstallFailure> = when (session.installerType) {
InstallerType.INTENT_BASED -> createIntentBasedInstallSession(session, needToCompleteIfSucceeded)
InstallerType.SESSION_BASED -> createSessionBasedInstallSession(session, needToCompleteIfSucceeded)
}

override fun resolveNotificationData(notificationData: NotificationData, name: String) = notificationData.run {
NotificationData.Builder()
.setTitle(
Expand All @@ -145,6 +155,97 @@ internal class InstallSessionFactoryImpl internal constructor(
}
return AckpinePromptInstallMessage
}

private fun createIntentBasedInstallSession(
installSession: SessionEntity.InstallSession,
needToCompleteIfSucceeded: Boolean
): IntentBasedInstallSession {
val id = UUID.fromString(installSession.session.id)
val initialState = installSession.getState(installSessionDao)
val initialProgress = installSession.getProgress(sessionProgressDao)
val session = IntentBasedInstallSession(
applicationContext,
apk = installSession.uris.singleOrNull()?.toUri() ?: throw SplitPackagesNotSupportedException(),
id, initialState, initialProgress,
installSession.session.confirmation, installSession.getNotificationData(),
lastUpdateTimestampDao, sessionDao,
sessionFailureDao = installSessionDao,
sessionProgressDao, executor, handler, installSession.notificationId!!,
BinarySemaphore()
)
if (!needToCompleteIfSucceeded || initialState.isTerminal) {
return session
}
// 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 packageName = installSession.packageName.orEmpty()
val lastUpdateTimestamp = installSession.lastUpdateTimestamp ?: Long.MAX_VALUE
val isSelfUpdate = initialState is Committed && applicationContext.packageName == packageName
val isLastUpdateTimestampUpdated = getLastSelfUpdateTimestamp() > lastUpdateTimestamp
val isSuccessfulSelfUpdate = isSelfUpdate && isLastUpdateTimestampUpdated
if (isSuccessfulSelfUpdate) {
session.complete(Succeeded)
}
if (isSuccessfulSelfUpdate) {
lastUpdateTimestampDao.setLastUpdateTimestamp(id.toString(), getLastSelfUpdateTimestamp())
}
return session
}

private fun createSessionBasedInstallSession(
installSession: SessionEntity.InstallSession,
needToCompleteIfSucceeded: Boolean
): SessionBasedInstallSession {
val initialState = installSession.getState(installSessionDao)
val initialProgress = installSession.getProgress(sessionProgressDao)
val nativeSessionId = installSession.nativeSessionId ?: -1
val session = SessionBasedInstallSession(
applicationContext,
apks = installSession.uris.map(String::toUri),
id = UUID.fromString(installSession.session.id),
initialState, initialProgress,
installSession.session.confirmation, installSession.getNotificationData(),
installSession.session.requireUserAction, installSession.getInstallMode(),
installSession.getPreapproval(), installSession.getConstraints(),
requestUpdateOwnership = installSession.requestUpdateOwnership ?: false,
packageSource = installSession.packageSource ?: PackageSource.Unspecified,
sessionDao,
sessionFailureDao = installSessionDao,
sessionProgressDao, nativeSessionIdDao, installPreapprovalDao, installConstraintsDao,
executor, handler, nativeSessionId, installSession.notificationId!!,
commitAttemptsCount = installSession.constraints?.commitAttemptsCount ?: 0,
isPreapproved = installSession.preapproval?.isPreapproved ?: false,
dbWriteSemaphore = BinarySemaphore()
)
if (!needToCompleteIfSucceeded || initialState.isTerminal) {
return session
}
val progressThreshold = (applicationContext.getSessionBasedSessionCommitProgressValue() * PROGRESS_MAX).toInt()
if (initialProgress.progress >= progressThreshold) {
// means that actual installation is ongoing or is completed
session.notifyCommitted() // block clients from committing
}
// If app is killed while installing but system installer activity remains visible,
// session is stuck in Committed state after new process start.
// Fails are guaranteed to be handled by PackageInstallerStatusReceiver (in case of self-update
// success is not handled), so if native session doesn't exist, it can only mean that it succeeded.
// There may be latency from the receiver, so we delay this to allow the receiver to kick in.
val packageInstaller = applicationContext.packageManager.packageInstaller
if (initialState is Committed && packageInstaller.getSessionInfo(nativeSessionId) == null) {
handler.postDelayed({ session.complete(Succeeded) }, 2000)
}
return session
}

private fun getLastSelfUpdateTimestamp(): Long {
return applicationContext.packageManager.getPackageInfo(applicationContext.packageName, 0).lastUpdateTime
}
}

private object AckpinePromptInstallTitle : ResolvableString.Resource() {
Expand Down
Loading

0 comments on commit 02adf26

Please sign in to comment.