Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit 5abb003e authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

refactor: move the install-state machine from AppInstallProcessor to AppInstallWorkRunner

Move the install-state machine and flow handling into a dedicated runner so worker behavior can be tested directly while the processor keeps the same external contract.
parent 1ca209e1
Loading
Loading
Loading
Loading
+2 −168
Original line number Diff line number Diff line
@@ -33,7 +33,6 @@ import foundation.e.apps.data.enums.Type
import foundation.e.apps.data.event.AppEvent
import foundation.e.apps.data.install.AppInstallComponents
import foundation.e.apps.data.install.AppManager
import foundation.e.apps.data.install.download.DownloadManagerUtils
import foundation.e.apps.data.install.models.AppInstall
import foundation.e.apps.data.install.notification.StorageNotificationManager
import foundation.e.apps.data.install.wrapper.AppEventDispatcher
@@ -44,7 +43,6 @@ import foundation.e.apps.data.preference.PlayStoreAuthStore
import foundation.e.apps.domain.model.User
import foundation.e.apps.domain.preferences.SessionRepository
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.transformWhile
import timber.log.Timber
import javax.inject.Inject

@@ -60,18 +58,11 @@ class AppInstallProcessor @Inject constructor(
    private val storageSpaceChecker: StorageSpaceChecker,
    private val networkStatusChecker: NetworkStatusChecker,
    private val appInstallAgeLimitGate: AppInstallAgeLimitGate,
    private val appUpdateCompletionHandler: AppUpdateCompletionHandler,
    private val appInstallWorkRunner: AppInstallWorkRunner,
) {
    @Inject
    lateinit var downloadManager: DownloadManagerUtils

    @Inject
    lateinit var appManager: AppManager

    companion object {
        private const val TAG = "AppInstallProcessor"
    }

    /**
     * creates [AppInstall] from [Application] and enqueues into WorkManager to run install process.
     * @param application represents the app info which will be installed
@@ -251,163 +242,6 @@ class AppInstallProcessor @Inject constructor(
        isItUpdateWork: Boolean,
        runInForeground: (suspend (String) -> Unit)
    ): Result<ResultStatus> {
        var appInstall: AppInstall? = null
        try {
            Timber.d("Fused download name $fusedDownloadId")

            appInstall = appInstallComponents.appInstallRepository.getDownloadById(fusedDownloadId)
            Timber.i(">>> dowork started for Fused download name " + appInstall?.name + " " + fusedDownloadId)

            appInstall?.let {
                checkDownloadingState(appInstall)

                val isUpdateWork =
                    isItUpdateWork && appInstallComponents.appManagerWrapper.isFusedDownloadInstalled(
                        appInstall
                    )

                if (!appInstall.isAppInstalling()) {
                    Timber.d("!!! returned")
                    return@let
                }

                if (!appInstallComponents.appManagerWrapper.validateFusedDownload(appInstall)) {
                    appInstallComponents.appManagerWrapper.installationIssue(it)
                    Timber.d("!!! installationIssue")
                    return@let
                }

                if (areFilesDownloadedButNotInstalled(appInstall)) {
                    Timber.i("===> Downloaded But not installed ${appInstall.name}")
                    appInstallComponents.appManagerWrapper.updateDownloadStatus(
                        appInstall,
                        Status.INSTALLING
                    )
                }

                runInForeground.invoke(it.name)

                startAppInstallationProcess(it, isUpdateWork)
            }
        } catch (e: Exception) {
            Timber.e(
                e,
                "Install worker is failed for ${appInstall?.packageName} exception: ${e.localizedMessage}"
            )
            appInstall?.let {
                appInstallComponents.appManagerWrapper.cancelDownload(appInstall)
            }
        }

        Timber.i("doWork: RESULT SUCCESS: ${appInstall?.name}")
        return Result.success(ResultStatus.OK)
    }

    @OptIn(DelicateCoroutinesApi::class)
    private fun checkDownloadingState(appInstall: AppInstall) {
        if (appInstall.status == Status.DOWNLOADING) {
            appInstall.downloadIdMap.keys.forEach { downloadId ->
                downloadManager.updateDownloadStatus(downloadId)
            }
        }
    }

    private fun areFilesDownloadedButNotInstalled(appInstall: AppInstall) =
        appInstall.areFilesDownloaded() && (!appInstallComponents.appManagerWrapper.isFusedDownloadInstalled(
            appInstall
        ) || appInstall.status == Status.INSTALLING)

    private suspend fun checkUpdateWork(appInstall: AppInstall?, isUpdateWork: Boolean) {
        appUpdateCompletionHandler.onInstallFinished(appInstall, isUpdateWork)
    }

    private suspend fun startAppInstallationProcess(appInstall: AppInstall, isUpdateWork: Boolean) {
        if (appInstall.isAwaiting()) {
            appInstallComponents.appManagerWrapper.downloadApp(appInstall, isUpdateWork)
            Timber.i("===> doWork: Download started ${appInstall.name} ${appInstall.status}")
        }

        appInstallComponents.appInstallRepository.getDownloadFlowById(appInstall.id)
            .transformWhile {
                emit(it)
                isInstallRunning(it)
            }
            .collect { latestFusedDownload ->
                handleFusedDownload(latestFusedDownload, appInstall, isUpdateWork)
            }
    }

    /**
     * Takes actions depending on the status of [AppInstall]
     *
     * @param latestAppInstall comes from Room database when [Status] is updated
     * @param appInstall is the original object when install process isn't started. It's used when [latestAppInstall]
     * becomes null, After installation is completed.
     */
    private suspend fun handleFusedDownload(
        latestAppInstall: AppInstall?,
        appInstall: AppInstall,
        isUpdateWork: Boolean
    ) {
        if (latestAppInstall == null) {
            Timber.d("===> download null: finish installation")
            finishInstallation(appInstall, isUpdateWork)
            return
        }

        handleFusedDownloadStatusCheckingException(latestAppInstall, isUpdateWork)
    }

    private fun isInstallRunning(it: AppInstall?) =
        it != null && it.status != Status.INSTALLATION_ISSUE

    private suspend fun handleFusedDownloadStatusCheckingException(
        download: AppInstall,
        isUpdateWork: Boolean
    ) {
        try {
            handleFusedDownloadStatus(download, isUpdateWork)
        } catch (e: Exception) {
            val message =
                "Handling install status is failed for ${download.packageName} exception: ${e.localizedMessage}"
            Timber.e(e, message)
            appInstallComponents.appManagerWrapper.installationIssue(download)
            finishInstallation(download, isUpdateWork)
        }
    }

    private suspend fun handleFusedDownloadStatus(appInstall: AppInstall, isUpdateWork: Boolean) {
        when (appInstall.status) {
            Status.AWAITING, Status.DOWNLOADING -> {
            }

            Status.DOWNLOADED -> {
                appInstallComponents.appManagerWrapper.updateDownloadStatus(
                    appInstall,
                    Status.INSTALLING
                )
            }

            Status.INSTALLING -> {
                Timber.i("===> doWork: Installing ${appInstall.name} ${appInstall.status}")
            }

            Status.INSTALLED, Status.INSTALLATION_ISSUE -> {
                Timber.i("===> doWork: Installed/Failed: ${appInstall.name} ${appInstall.status}")
                finishInstallation(appInstall, isUpdateWork)
            }

            else -> {
                Timber.wtf(
                    TAG,
                    "===> ${appInstall.name} is in wrong state ${appInstall.status}"
                )
                finishInstallation(appInstall, isUpdateWork)
            }
        }
    }

    private suspend fun finishInstallation(appInstall: AppInstall, isUpdateWork: Boolean) {
        checkUpdateWork(appInstall, isUpdateWork)
        return appInstallWorkRunner.processInstall(fusedDownloadId, isItUpdateWork, runInForeground)
    }
}
+176 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.data.install.workmanager

import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.install.AppInstallRepository
import foundation.e.apps.data.install.AppManagerWrapper
import foundation.e.apps.data.install.download.DownloadManagerUtils
import foundation.e.apps.data.install.models.AppInstall
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.transformWhile
import timber.log.Timber
import javax.inject.Inject

@Suppress("TooGenericExceptionCaught") // FIXME: Remove suppression and fix detekt
class AppInstallWorkRunner @Inject constructor(
    private val appInstallRepository: AppInstallRepository,
    private val appManagerWrapper: AppManagerWrapper,
    private val downloadManager: DownloadManagerUtils,
    private val appUpdateCompletionHandler: AppUpdateCompletionHandler,
) {
    @OptIn(DelicateCoroutinesApi::class)
    suspend fun processInstall(
        fusedDownloadId: String,
        isItUpdateWork: Boolean,
        runInForeground: suspend (String) -> Unit
    ): Result<ResultStatus> {
        var appInstall: AppInstall? = null
        try {
            Timber.d("Fused download name $fusedDownloadId")

            appInstall = appInstallRepository.getDownloadById(fusedDownloadId)
            Timber.i(">>> doWork started for Fused download name ${appInstall?.name} $fusedDownloadId")

            appInstall?.let {
                checkDownloadingState(appInstall)

                val isUpdateWork =
                    isItUpdateWork && appManagerWrapper.isFusedDownloadInstalled(appInstall)

                if (!appInstall.isAppInstalling()) {
                    Timber.d("!!! returned")
                    return@let
                }

                if (!appManagerWrapper.validateFusedDownload(appInstall)) {
                    appManagerWrapper.installationIssue(it)
                    Timber.d("!!! installationIssue")
                    return@let
                }

                if (areFilesDownloadedButNotInstalled(appInstall)) {
                    Timber.i("===> Downloaded But not installed ${appInstall.name}")
                    appManagerWrapper.updateDownloadStatus(appInstall, Status.INSTALLING)
                }

                runInForeground.invoke(it.name)

                startAppInstallationProcess(it, isUpdateWork)
            }
        } catch (e: Exception) {
            Timber.e(
                e,
                "Install worker is failed for ${appInstall?.packageName} exception: ${e.localizedMessage}"
            )
            appInstall?.let {
                appManagerWrapper.cancelDownload(appInstall)
            }
        }

        Timber.i("doWork: RESULT SUCCESS: ${appInstall?.name}")
        return Result.success(ResultStatus.OK)
    }

    @OptIn(DelicateCoroutinesApi::class)
    private fun checkDownloadingState(appInstall: AppInstall) {
        if (appInstall.status == Status.DOWNLOADING) {
            appInstall.downloadIdMap.keys.forEach { downloadId ->
                downloadManager.updateDownloadStatus(downloadId)
            }
        }
    }

    private fun areFilesDownloadedButNotInstalled(appInstall: AppInstall): Boolean = appInstall.areFilesDownloaded() &&
        (!appManagerWrapper.isFusedDownloadInstalled(appInstall) || appInstall.status == Status.INSTALLING)

    private suspend fun startAppInstallationProcess(appInstall: AppInstall, isUpdateWork: Boolean) {
        if (appInstall.isAwaiting()) {
            appManagerWrapper.downloadApp(appInstall, isUpdateWork)
            Timber.i("===> doWork: Download started ${appInstall.name} ${appInstall.status}")
        }

        appInstallRepository.getDownloadFlowById(appInstall.id)
            .transformWhile {
                emit(it)
                isInstallRunning(it)
            }
            .collect { latestFusedDownload ->
                handleFusedDownload(latestFusedDownload, appInstall, isUpdateWork)
            }
    }

    private suspend fun handleFusedDownload(
        latestAppInstall: AppInstall?,
        appInstall: AppInstall,
        isUpdateWork: Boolean
    ) {
        if (latestAppInstall == null) {
            Timber.d("===> download null: finish installation")
            finishInstallation(appInstall, isUpdateWork)
            return
        }

        handleFusedDownloadStatusCheckingException(latestAppInstall, isUpdateWork)
    }

    private fun isInstallRunning(it: AppInstall?) =
        it != null && it.status != Status.INSTALLATION_ISSUE

    private suspend fun handleFusedDownloadStatusCheckingException(
        download: AppInstall,
        isUpdateWork: Boolean
    ) {
        try {
            handleFusedDownloadStatus(download, isUpdateWork)
        } catch (e: Exception) {
            val message =
                "Handling install status is failed for ${download.packageName} exception: ${e.localizedMessage}"
            Timber.e(e, message)
            appManagerWrapper.installationIssue(download)
            finishInstallation(download, isUpdateWork)
        }
    }

    private suspend fun handleFusedDownloadStatus(appInstall: AppInstall, isUpdateWork: Boolean) {
        when (appInstall.status) {
            Status.AWAITING, Status.DOWNLOADING -> Unit
            Status.DOWNLOADED -> appManagerWrapper.updateDownloadStatus(
                appInstall,
                Status.INSTALLING
            )

            Status.INSTALLING -> Timber.i("===> doWork: Installing ${appInstall.name} ${appInstall.status}")
            Status.INSTALLED, Status.INSTALLATION_ISSUE -> {
                Timber.i("===> doWork: Installed/Failed: ${appInstall.name} ${appInstall.status}")
                finishInstallation(appInstall, isUpdateWork)
            }

            else -> {
                Timber.w("===> ${appInstall.name} is in wrong state ${appInstall.status}")
                finishInstallation(appInstall, isUpdateWork)
            }
        }
    }

    private suspend fun finishInstallation(appInstall: AppInstall, isUpdateWork: Boolean) {
        appUpdateCompletionHandler.onInstallFinished(appInstall, isUpdateWork)
    }
}
+19 −3
Original line number Diff line number Diff line
@@ -21,8 +21,8 @@ package foundation.e.apps.installProcessor
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.aurora.gplayapi.data.models.AuthData
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.R
import foundation.e.apps.data.ResultSupreme
import foundation.e.apps.data.application.ApplicationRepository
import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.Source
@@ -37,6 +37,7 @@ import foundation.e.apps.data.install.models.AppInstall
import foundation.e.apps.data.install.notification.StorageNotificationManager
import foundation.e.apps.data.install.workmanager.AppInstallAgeLimitGate
import foundation.e.apps.data.install.workmanager.AppInstallProcessor
import foundation.e.apps.data.install.workmanager.AppInstallWorkRunner
import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler
import foundation.e.apps.data.install.workmanager.InstallWorkManager
import foundation.e.apps.data.install.wrapper.NetworkStatusChecker
@@ -117,6 +118,7 @@ class AppInstallProcessorTest {
    private lateinit var networkStatusChecker: NetworkStatusChecker
    private lateinit var appInstallAgeLimitGate: AppInstallAgeLimitGate
    private lateinit var appUpdateCompletionHandler: AppUpdateCompletionHandler
    private lateinit var appInstallWorkRunner: AppInstallWorkRunner

    @Before
    fun setup() {
@@ -153,6 +155,14 @@ class AppInstallProcessorTest {
            updatesTracker,
            updatesNotificationSender
        )
        val downloadManager =
            mockk<foundation.e.apps.data.install.download.DownloadManagerUtils>(relaxed = true)
        appInstallWorkRunner = AppInstallWorkRunner(
            appInstallRepository,
            fakeFusedManagerRepository,
            downloadManager,
            appUpdateCompletionHandler
        )

        appInstallProcessor = AppInstallProcessor(
            context,
@@ -165,7 +175,7 @@ class AppInstallProcessorTest {
            storageSpaceChecker,
            networkStatusChecker,
            appInstallAgeLimitGate,
            appUpdateCompletionHandler
            appInstallWorkRunner
        )
    }

@@ -639,6 +649,12 @@ class AppInstallProcessorTest {
            appEventDispatcher,
            parentalControlAuthGateway
        )
        val workRunner = AppInstallWorkRunner(
            appInstallRepository,
            appManagerWrapper,
            mockk(relaxed = true),
            appUpdateCompletionHandler
        )
        return AppInstallProcessor(
            context,
            appInstallComponents,
@@ -650,7 +666,7 @@ class AppInstallProcessorTest {
            storageSpaceChecker,
            networkStatusChecker,
            ageLimitGate,
            appUpdateCompletionHandler
            workRunner
        )
    }
}
+209 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.installProcessor

import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import foundation.e.apps.data.enums.ResultStatus
import foundation.e.apps.data.enums.Status
import foundation.e.apps.data.fdroid.FDroidRepository
import foundation.e.apps.data.install.AppInstallRepository
import foundation.e.apps.data.install.AppManager
import foundation.e.apps.data.install.download.DownloadManagerUtils
import foundation.e.apps.data.install.models.AppInstall
import foundation.e.apps.data.install.workmanager.AppInstallWorkRunner
import foundation.e.apps.data.install.workmanager.AppUpdateCompletionHandler
import foundation.e.apps.util.MainCoroutineRule
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
class AppInstallWorkRunnerTest {
    @Rule
    @JvmField
    val instantExecutorRule = InstantTaskExecutorRule()

    @get:Rule
    var mainCoroutineRule = MainCoroutineRule()

    private lateinit var fakeFusedDownloadDAO: FakeAppInstallDAO
    private lateinit var appInstallRepository: AppInstallRepository
    private lateinit var fakeFusedManagerRepository: FakeAppManagerWrapper
    private lateinit var downloadManagerUtils: DownloadManagerUtils
    private lateinit var appUpdateCompletionHandler: AppUpdateCompletionHandler
    private lateinit var workRunner: AppInstallWorkRunner
    private lateinit var context: Context

    @Mock
    private lateinit var fakeFusedManager: AppManager

    @Mock
    private lateinit var fakeFDroidRepository: FDroidRepository

    @Before
    fun setup() {
        MockitoAnnotations.openMocks(this)
        context = mockk(relaxed = true)
        fakeFusedDownloadDAO = FakeAppInstallDAO()
        appInstallRepository = AppInstallRepository(fakeFusedDownloadDAO)
        fakeFusedManagerRepository =
            FakeAppManagerWrapper(fakeFusedDownloadDAO, context, fakeFusedManager, fakeFDroidRepository)
        downloadManagerUtils = mockk(relaxed = true)
        appUpdateCompletionHandler = mockk(relaxed = true)
        workRunner = AppInstallWorkRunner(
            appInstallRepository,
            fakeFusedManagerRepository,
            downloadManagerUtils,
            appUpdateCompletionHandler
        )
    }

    @Test
    fun processInstall_completesNormalFlow() = runTest {
        val fusedDownload = initTest()

        val finalFusedDownload = runProcessInstall(fusedDownload)

        assertTrue(finalFusedDownload == null)
    }

    @Test
    fun processInstall_keepsBlockedDownloadUntouched() = runTest {
        val fusedDownload = initTest()
        fusedDownload.status = Status.BLOCKED

        val finalFusedDownload = runProcessInstall(fusedDownload)

        assertEquals(Status.BLOCKED, finalFusedDownload?.status)
    }

    @Test
    fun processInstall_marksDownloadedFilesAsInstalling() = runTest {
        val fusedDownload = initTest()
        fusedDownload.downloadIdMap = mutableMapOf(Pair(231, true))

        val finalFusedDownload = runProcessInstall(fusedDownload)

        assertTrue(finalFusedDownload == null)
    }

    @Test
    fun processInstall_reportsInvalidPackageAsInstallationIssue() = runTest {
        val fusedDownload = initTest(packageName = "")
        fusedDownload.downloadIdMap = mutableMapOf(Pair(231, true))

        val finalFusedDownload = runProcessInstall(fusedDownload)

        assertEquals(Status.INSTALLATION_ISSUE, finalFusedDownload?.status)
    }

    @Test
    fun processInstall_reportsMissingDownloadUrlsAsInstallationIssue() = runTest {
        val fusedDownload = initTest(downloadUrlList = mutableListOf())

        val finalFusedDownload = runProcessInstall(fusedDownload)

        assertEquals(Status.INSTALLATION_ISSUE, finalFusedDownload?.status)
    }

    @Test
    fun processInstall_returnsSuccessWhenInternalExceptionOccurs() = runTest {
        val fusedDownload = initTest()
        fakeFusedManagerRepository.forceCrash = true

        val result = workRunner.processInstall(fusedDownload.id, false) {
            // _ignored_
        }
        val finalFusedDownload = fakeFusedDownloadDAO.getDownloadById(fusedDownload.id)

        assertTrue(result.isSuccess)
        assertEquals(ResultStatus.OK, result.getOrNull())
        assertTrue(finalFusedDownload == null || fusedDownload.status == Status.INSTALLATION_ISSUE)
    }

    @Test
    fun processInstall_reportsDownloadFailure() = runTest {
        val fusedDownload = initTest()
        fakeFusedManagerRepository.willDownloadFail = true

        val finalFusedDownload = runProcessInstall(fusedDownload)

        assertEquals(Status.INSTALLATION_ISSUE, finalFusedDownload?.status)
    }

    @Test
    fun processInstall_reportsInstallFailure() = runTest {
        val fusedDownload = initTest()
        fakeFusedManagerRepository.willInstallFail = true

        val finalFusedDownload = runProcessInstall(fusedDownload)

        assertEquals(Status.INSTALLATION_ISSUE, finalFusedDownload?.status)
    }

    @Test
    fun processInstall_updatesDownloadManagerStateForDownloadingItems() = runTest {
        val fusedDownload = initTest()
        fusedDownload.status = Status.DOWNLOADING
        fusedDownload.downloadURLList = mutableListOf()
        fusedDownload.downloadIdMap = mutableMapOf(231L to false, 232L to false)

        workRunner.processInstall(fusedDownload.id, false) {
            // _ignored_
        }

        verify { downloadManagerUtils.updateDownloadStatus(231L) }
        verify { downloadManagerUtils.updateDownloadStatus(232L) }
    }

    private suspend fun initTest(
        packageName: String? = null,
        downloadUrlList: MutableList<String>? = null
    ): AppInstall {
        val fusedDownload = createFusedDownload(packageName, downloadUrlList)
        fakeFusedDownloadDAO.addDownload(fusedDownload)
        return fusedDownload
    }

    private suspend fun runProcessInstall(appInstall: AppInstall): AppInstall? {
        workRunner.processInstall(appInstall.id, false) {
            // _ignored_
        }
        return fakeFusedDownloadDAO.getDownloadById(appInstall.id)
    }

    private fun createFusedDownload(
        packageName: String? = null,
        downloadUrlList: MutableList<String>? = null
    ) = AppInstall(
        id = "121",
        status = Status.AWAITING,
        downloadURLList = downloadUrlList ?: mutableListOf("apk1", "apk2"),
        packageName = packageName ?: "com.unit.test"
    )
}