diff --git a/.circleci/config.yml b/.circleci/config.yml index 732b94361..406776fec 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -52,7 +52,7 @@ reference: android_config: &android_config working_directory: *workspace docker: - - image: circleci/android:api-28-ndk + - image: circleci/android@sha256:fa7a00c75f4b28cc4f2a15a382fb76e830d62a77efd1a3f8549f7f5fdad4ca44 environment: TERM: dumb # Limit JVM heap size to prevent exceeding container memory @@ -345,4 +345,4 @@ workflows: requires: - test_unit - test_instrumented - - build_production_release \ No newline at end of file + - build_production_release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 350ebf81c..7d2bafd16 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ These are intended as just that: guidelines, so use your best judgement when sub All contributions must follow the UserLAnd [Code of Conduct](https://github.com/CypherpunkArmory/UserLAnd/blob/master/CODE_OF_CONDUCT.md). ## Connect with us -If you have any questions please join us on our [slack](https://communityinviter.com/apps/userlandtech/userland) #contributors channel +Please talk with us here in the form of a PR or an issue. ## Architecture We follow the MVVM-C architecture in UserLAnd. UI updates should exist exclusively within XML and be inflated exclusively from diff --git a/README.md b/README.md index 0663b1f0b..c7ec0b792 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ Features: * Install and uninstall like a regular app. * No root required. -[Get it on F-Droid](https://f-droid.org/packages/tech.ula/) +[Get it on F-Droid](https://f-droid.org/packages/tech.ula) [Get it on Google Play](https://play.google.com/store/apps/details?id=tech.ula) @@ -18,7 +18,6 @@ Features: ## Have a bug report or a feature request? You can see our templates by visiting our [issue center](https://github.com/CypherpunkArmory/UserLAnd/issues). -You can also chat with us on [slack](https://communityinviter.com/apps/userlandtech/userland). ## Want to contribute? See our [CONTRIBUTING](https://github.com/CypherpunkArmory/UserLAnd/blob/master/CONTRIBUTING.md) document. diff --git a/app/build.gradle b/app/build.gradle index 9d3da64fb..5110da5e2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,7 +24,7 @@ android { minSdkVersion 21 targetSdkVersion 29 versionCode vcode - versionName "2.6.2" + versionName "2.6.3" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArguments clearPackageData: 'true' @@ -198,7 +198,7 @@ ext.architectures = ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"] ext.libDir = "$project.projectDir/src/main/jniLibs" task downloadAssets(type: Download) { - def assetVersion = "v1.0.1" + def assetVersion = "v1.1.0" def baseUrl = "https://github.com/CypherpunkArmory/UserLAnd-Assets-Support/releases/download/$assetVersion" for (arch in architectures) { src "$baseUrl/$arch-assets.zip" @@ -222,6 +222,7 @@ task fetchAssets(dependsOn: downloadAssets) { // Lib files must start with 'lib' and end with '.so.' rename '(.*)', 'lib_$1.so' } + new File("$libDir/$arch","lib_arch.so").text = "$arch" } } } @@ -280,7 +281,6 @@ dependencies { def mockito_version = '2.23.0' def mockito_kotlin_version = '2.1.0' def core_testing_version = '2.0.0-beta01' - def espresso_version = '3.2.0' def androidx_test_version = '1.2.0' def androidx_test_ext_version = '1.1.0' @@ -296,6 +296,7 @@ dependencies { androidTestImplementation "androidx.test:rules:$androidx_test_version" androidTestImplementation "androidx.test.ext:junit:$androidx_test_ext_version" // Barista packages espresso-core and espresso-contrib + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.2.0' androidTestImplementation('com.schibsted.spain:barista:3.1.0') { exclude group: 'com.android.support' exclude group: 'org.jetbrains.kotlin' diff --git a/app/src/androidTest/java/tech/ula/ui/MainActivityTest.kt b/app/src/androidTest/java/tech/ula/ui/MainActivityTest.kt index f011f9274..effa68c65 100644 --- a/app/src/androidTest/java/tech/ula/ui/MainActivityTest.kt +++ b/app/src/androidTest/java/tech/ula/ui/MainActivityTest.kt @@ -1,10 +1,14 @@ package tech.ula.ui import android.Manifest +import android.content.Intent +import android.net.Uri import androidx.test.espresso.Espresso +import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.test.espresso.intent.Intents.intended +import androidx.test.espresso.intent.matcher.IntentMatchers.* // ktlint-disable no-wildcard-imports import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.LargeTest -import androidx.test.rule.ActivityTestRule import androidx.test.rule.GrantPermissionRule import com.schibsted.spain.barista.assertion.BaristaListAssertions.assertDisplayedAtPosition import com.schibsted.spain.barista.assertion.BaristaVisibilityAssertions.assertNotDisplayed @@ -27,7 +31,7 @@ import java.io.File class MainActivityTest { @get:Rule - val activityRule = ActivityTestRule(MainActivity::class.java) + val intentTestRule = IntentsTestRule(MainActivity::class.java) // Permissions are granted automatically by firebase, so to keep parity we skip that step // locally as well. @@ -47,12 +51,12 @@ class MainActivityTest { @Before fun setup() { - activity = activityRule.activity + activity = intentTestRule.activity homeDirectory = File(activity.filesDir, homeLocation) } @Test - fun testHappyPath() { + fun test_ssh_session_can_be_started() { R.id.app_list_fragment.shortWaitForDisplay() R.id.swipe_refresh.waitForRefresh(activity) @@ -121,6 +125,58 @@ class MainActivityTest { R.id.terminal_view.shortWaitForDisplay() } + @Test + fun test_vnc_session_can_be_started() { + R.id.app_list_fragment.shortWaitForDisplay() + + R.id.swipe_refresh.waitForRefresh(activity) + + // Click alpine + assertDisplayedAtPosition(R.id.list_apps, 0, R.id.apps_name, appName) + clickListItem(R.id.list_apps, 0) + + // Set filesystem credentials + R.string.filesystem_credentials_reasoning.waitForDisplay() + writeTo(R.id.text_input_username, username) + writeTo(R.id.text_input_password, sshPassword) + writeTo(R.id.text_input_vnc_password, vncPassword) + clickDialogPositiveButton() + + // Set session type to vnc + R.string.prompt_app_connection_type_preference.shortWaitForDisplay() + clickRadioButtonItem(R.id.radio_apps_service_type_preference, R.id.vnc_radio_button) + clickDialogPositiveButton() + + // Wait for progress dialog to complete + R.id.progress_bar_session_list.shortWaitForDisplay() + R.string.progress_downloading.longWaitForDisplay() + R.string.progress_copying_downloads.extraLongWaitForDisplay() + R.string.progress_verifying_assets.waitForDisplay() + R.string.progress_setting_up_filesystem.waitForDisplay() + R.string.progress_starting.longWaitForDisplay() + Thread.sleep(10000) + + val clientIntent = Intent() + clientIntent.action = Intent.ACTION_VIEW + clientIntent.type = "application/vnd.vnc" + clientIntent.data = Uri.parse("vnc://127.0.0.1:5951/?VncUsername=$username&VncPassword=$vncPassword") + clientIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + val packageManager = activity.packageManager + val activities = packageManager.queryIntentActivities(clientIntent, 0) + if (activities.size > 0) { + // Match case where bVNC is present, e.g. test is running on personal device + intended(hasAction(Intent.ACTION_VIEW)) + intended(hasData(Uri.parse("vnc://127.0.0.1:5951/?VncUsername=$username&VncPassword=$vncPassword"))) + intended(hasFlag(Intent.FLAG_ACTIVITY_NEW_TASK)) + } else { + // Match case where bVNC is not present on device, e.g. firebase + val packageName = "com.iiordanov.freebVNC" + intended(hasAction(Intent.ACTION_VIEW)) + intended(hasData(Uri.parse("market://details?id=$packageName"))) + intended(hasFlag(Intent.FLAG_ACTIVITY_NEW_TASK)) + } + } + private fun doHappyPathTestScript(): List { val startedFile = File(homeDirectory, "test.scriptStarted") val updatedFile = File(homeDirectory, "test.apkUpdated") diff --git a/app/src/main/java/tech/ula/MainActivity.kt b/app/src/main/java/tech/ula/MainActivity.kt index 1b45e0ed7..32f2f86c1 100644 --- a/app/src/main/java/tech/ula/MainActivity.kt +++ b/app/src/main/java/tech/ula/MainActivity.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.launch import tech.ula.model.entities.App import tech.ula.model.entities.ServiceType import tech.ula.model.entities.Session +import tech.ula.model.remote.GithubApiClient import tech.ula.model.repositories.AssetRepository import tech.ula.model.repositories.UlaDatabase import tech.ula.model.state.* // ktlint-disable no-wildcard-imports @@ -117,7 +118,8 @@ class MainActivity : AppCompatActivity(), SessionListFragment.SessionSelection, val ulaDatabase = UlaDatabase.getInstance(this) val assetPreferences = AssetPreferences(this) - val assetRepository = AssetRepository(filesDir.path, assetPreferences) + val githubApiClient = GithubApiClient(ulaFiles) + val assetRepository = AssetRepository(filesDir.path, assetPreferences, githubApiClient) val filesystemManager = FilesystemManager(ulaFiles, busyboxExecutor) val storageCalculator = StorageCalculator(StatFs(filesDir.path)) @@ -126,7 +128,7 @@ class MainActivity : AppCompatActivity(), SessionListFragment.SessionSelection, val downloadManagerWrapper = DownloadManagerWrapper(downloadManager) val assetDownloader = AssetDownloader(assetPreferences, downloadManagerWrapper, ulaFiles) - val appsStartupFsm = AppsStartupFsm(ulaDatabase, filesystemManager) + val appsStartupFsm = AppsStartupFsm(ulaDatabase, filesystemManager, ulaFiles) val sessionStartupFsm = SessionStartupFsm(ulaDatabase, assetRepository, filesystemManager, assetDownloader, storageCalculator) ViewModelProviders.of(this, MainActivityViewModelFactory(appsStartupFsm, sessionStartupFsm)) .get(MainActivityViewModel::class.java) diff --git a/app/src/main/java/tech/ula/ServerService.kt b/app/src/main/java/tech/ula/ServerService.kt index 7ba657a14..c19db306d 100644 --- a/app/src/main/java/tech/ula/ServerService.kt +++ b/app/src/main/java/tech/ula/ServerService.kt @@ -6,17 +6,19 @@ import android.content.Intent import android.net.Uri import android.os.IBinder import androidx.localbroadcastmanager.content.LocalBroadcastManager -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import kotlinx.coroutines.* // ktlint-disable no-wildcard-imports import tech.ula.model.entities.App import tech.ula.model.entities.ServiceType import tech.ula.model.repositories.UlaDatabase import tech.ula.model.entities.Session import tech.ula.utils.* // ktlint-disable no-wildcard-imports +import kotlin.coroutines.CoroutineContext -class ServerService : Service() { +class ServerService : Service(), CoroutineScope { + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = Dispatchers.Default + job companion object { const val SERVER_SERVICE_RESULT: String = "tech.ula.ServerService.RESULT" @@ -53,9 +55,8 @@ class ServerService : Service() { when (intent?.getStringExtra("type")) { "start" -> { - val coroutineScope = CoroutineScope(Dispatchers.Default) val session: Session = intent.getParcelableExtra("session")!! - coroutineScope.launch { startSession(session) } + this.launch { startSession(session) } } "stopApp" -> { val app: App = intent.getParcelableExtra("app")!! @@ -87,10 +88,18 @@ class ServerService : Service() { // to clean up when app is swiped away. override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) + // Redundancy to ensure no hanging processes, given broad device spectrum. + this.coroutineContext.cancel() stopForeground(true) stopSelf() } + override fun onDestroy() { + super.onDestroy() + // Redundancy to ensure no hanging processes, given broad device spectrum. + this.coroutineContext.cancel() + } + private fun removeSession(session: Session) { activeSessions.remove(session.pid) if (activeSessions.isEmpty()) { @@ -145,8 +154,8 @@ class ServerService : Service() { private fun startSshClient(session: Session) { val connectBotIntent = Intent() - connectBotIntent.action = "android.intent.action.VIEW" - connectBotIntent.data = Uri.parse("ssh://${session.username}@localhost:${session.port}/#userland") + connectBotIntent.action = Intent.ACTION_VIEW + connectBotIntent.data = Uri.parse("ssh://${session.username}@localhost:2022/#userland") connectBotIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK startActivity(connectBotIntent) @@ -154,7 +163,7 @@ class ServerService : Service() { private fun startVncClient(session: Session, packageName: String) { val bVncIntent = Intent() - bVncIntent.action = "android.intent.action.VIEW" + bVncIntent.action = Intent.ACTION_VIEW bVncIntent.type = "application/vnd.vnc" bVncIntent.data = Uri.parse("vnc://127.0.0.1:5951/?VncUsername=${session.username}&VncPassword=${session.vncPassword}") bVncIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK diff --git a/app/src/main/java/tech/ula/model/entities/Session.kt b/app/src/main/java/tech/ula/model/entities/Session.kt index dcfeb9ed1..485df36ba 100644 --- a/app/src/main/java/tech/ula/model/entities/Session.kt +++ b/app/src/main/java/tech/ula/model/entities/Session.kt @@ -77,7 +77,7 @@ data class Session( var password: String = "", var vncPassword: String = "", var serviceType: ServiceType = ServiceType.Unselected, - var port: Long = 2022, + var port: Long = 2022, // TODO This can be removed. Any eventual port managing should be done at a high er abstraction. var pid: Long = 0, var geometry: String = "", val isAppsSession: Boolean = false diff --git a/app/src/main/java/tech/ula/model/remote/GithubApiClient.kt b/app/src/main/java/tech/ula/model/remote/GithubApiClient.kt index 19b8821a2..5907eec3e 100644 --- a/app/src/main/java/tech/ula/model/remote/GithubApiClient.kt +++ b/app/src/main/java/tech/ula/model/remote/GithubApiClient.kt @@ -7,9 +7,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import okhttp3.Request -import tech.ula.utils.DeviceArchitecture import tech.ula.utils.Logger import tech.ula.utils.SentryLogger +import tech.ula.utils.UlaFiles import java.io.IOException import java.net.UnknownHostException @@ -20,7 +20,7 @@ class UrlProvider { } class GithubApiClient( - private val deviceArchitecture: DeviceArchitecture = DeviceArchitecture(), + private val ulaFiles: UlaFiles, private val urlProvider: UrlProvider = UrlProvider(), private val logger: Logger = SentryLogger() ) { @@ -39,7 +39,7 @@ class GithubApiClient( suspend fun getAssetsListDownloadUrl(repo: String): String = withContext(Dispatchers.IO) { val result = latestResults[repo] ?: queryLatestRelease(repo) - return@withContext result.assets.find { it.name == "${deviceArchitecture.getArchType()}-assets.txt" }!!.downloadUrl + return@withContext result.assets.find { it.name == "${ulaFiles.getArchType()}-assets.txt" }!!.downloadUrl } @Throws(IOException::class) @@ -52,7 +52,7 @@ class GithubApiClient( @Throws(IOException::class) suspend fun getAssetEndpoint(assetType: String, repo: String): String = withContext(Dispatchers.IO) { val result = latestResults[repo] ?: queryLatestRelease(repo) - val assetName = "${deviceArchitecture.getArchType()}-$assetType" + val assetName = "${ulaFiles.getArchType()}-$assetType" return@withContext result.assets.find { it.name == assetName }!!.downloadUrl } diff --git a/app/src/main/java/tech/ula/model/repositories/AssetRepository.kt b/app/src/main/java/tech/ula/model/repositories/AssetRepository.kt index cd4a5e97b..1a424c61a 100644 --- a/app/src/main/java/tech/ula/model/repositories/AssetRepository.kt +++ b/app/src/main/java/tech/ula/model/repositories/AssetRepository.kt @@ -24,7 +24,7 @@ data class DownloadMetadata( class AssetRepository( private val applicationFilesDirPath: String, private val assetPreferences: AssetPreferences, - private val githubApiClient: GithubApiClient = GithubApiClient(), + private val githubApiClient: GithubApiClient, private val httpStream: HttpStream = HttpStream(), private val logger: Logger = SentryLogger() ) { diff --git a/app/src/main/java/tech/ula/model/state/AppsStartupFsm.kt b/app/src/main/java/tech/ula/model/state/AppsStartupFsm.kt index 777e66d03..f7e0dd956 100644 --- a/app/src/main/java/tech/ula/model/state/AppsStartupFsm.kt +++ b/app/src/main/java/tech/ula/model/state/AppsStartupFsm.kt @@ -16,7 +16,7 @@ import tech.ula.utils.* // ktlint-disable no-wildcard-imports class AppsStartupFsm( ulaDatabase: UlaDatabase, private val filesystemManager: FilesystemManager, - private val deviceArchitecture: DeviceArchitecture = DeviceArchitecture(), + private val ulaFiles: UlaFiles, private val logger: Logger = SentryLogger() ) { @@ -114,7 +114,6 @@ class AppsStartupFsm( private suspend fun setServiceType(appSession: Session, serviceType: ServiceType) = withContext(Dispatchers.IO) { appSession.serviceType = serviceType - appSession.port = if (serviceType == ServiceType.Ssh) 2022 else 51 sessionDao.updateSession(appSession) state.postValue(AppHasServiceTypeSet) } @@ -124,7 +123,7 @@ class AppsStartupFsm( val potentialAppFilesystem = filesystemDao.findAppsFilesystemByType(app.filesystemRequired) if (potentialAppFilesystem.isEmpty()) { - val deviceArchitecture = deviceArchitecture.getArchType() + val deviceArchitecture = ulaFiles.getArchType() val fsToInsert = Filesystem(0, name = "apps", archType = deviceArchitecture, distributionType = app.filesystemRequired, isAppsFilesystem = true) filesystemDao.insertFilesystem(fsToInsert) @@ -157,7 +156,6 @@ class AppsStartupFsm( state.postValue(SyncingDatabaseEntries) appSession.filesystemId = appsFilesystem.id appSession.filesystemName = appsFilesystem.name - appSession.port = if (appSession.serviceType is ServiceType.Ssh) 2022 else 51 appSession.username = appsFilesystem.defaultUsername appSession.password = appsFilesystem.defaultPassword appSession.vncPassword = appsFilesystem.defaultVncPassword diff --git a/app/src/main/java/tech/ula/ui/FilesystemEditFragment.kt b/app/src/main/java/tech/ula/ui/FilesystemEditFragment.kt index ea6b1dc9a..a227c585a 100644 --- a/app/src/main/java/tech/ula/ui/FilesystemEditFragment.kt +++ b/app/src/main/java/tech/ula/ui/FilesystemEditFragment.kt @@ -24,9 +24,9 @@ import kotlinx.android.synthetic.main.frag_filesystem_edit.* import tech.ula.MainActivity import tech.ula.R import tech.ula.model.repositories.UlaDatabase -import tech.ula.utils.DeviceArchitecture import tech.ula.utils.PermissionHandler import tech.ula.utils.CredentialValidator +import tech.ula.utils.UlaFiles import tech.ula.utils.preferences.AppsPreferences import tech.ula.viewmodel.FilesystemImportStatus import tech.ula.viewmodel.ImportSuccess @@ -232,12 +232,8 @@ class FilesystemEditFragment : Fragment() { filesystemEditViewModel.updateFilesystem(filesystem) navController.popBackStack() } else { - try { - filesystem.archType = DeviceArchitecture().getArchType() - } catch (err: Exception) { - Toast.makeText(activityContext, R.string.no_supported_architecture, Toast.LENGTH_LONG).show() - return true - } + val ulaFiles = UlaFiles(activityContext, activityContext.applicationInfo.nativeLibraryDir) + filesystem.archType = ulaFiles.getArchType() if (filesystem.isCreatedFromBackup) { filesystemEditViewModel.insertFilesystemFromBackup(activityContext.contentResolver, filesystem, activityContext.filesDir) } else { diff --git a/app/src/main/java/tech/ula/ui/SessionEditFragment.kt b/app/src/main/java/tech/ula/ui/SessionEditFragment.kt index d26823d10..34d4bae59 100644 --- a/app/src/main/java/tech/ula/ui/SessionEditFragment.kt +++ b/app/src/main/java/tech/ula/ui/SessionEditFragment.kt @@ -153,7 +153,6 @@ class SessionEditFragment : Fragment() { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { val selectedServiceType = parent?.getItemAtPosition(position).toString().toServiceType() session.serviceType = selectedServiceType - session.port = getDefaultServicePort(selectedServiceType) } } diff --git a/app/src/main/java/tech/ula/utils/DeviceArchitecture.kt b/app/src/main/java/tech/ula/utils/DeviceArchitecture.kt deleted file mode 100644 index a655c3b47..000000000 --- a/app/src/main/java/tech/ula/utils/DeviceArchitecture.kt +++ /dev/null @@ -1,41 +0,0 @@ -package tech.ula.utils - -import android.os.Build - -class DeviceArchitecture { - fun getArchType(): String { - val supportedABIS = this.getSupportedAbis() - .map { - translateABI(it) - } - .filter { - isSupported(it) - } - return if (supportedABIS.size == 1 && supportedABIS[0] == "") { - val exception = IllegalStateException("No supported ABI!") - SentryLogger().addExceptionBreadcrumb(exception) - throw exception - } else { - supportedABIS[0] - } - } - - private fun getSupportedAbis(): Array { - return Build.SUPPORTED_ABIS - } - - private fun isSupported(abi: String): Boolean { - val supportedABIs = listOf("arm64", "arm", "x86_64", "x86") - return supportedABIs.contains(abi) - } - - private fun translateABI(abi: String): String { - return when (abi) { - "arm64-v8a" -> "arm64" - "armeabi-v7a" -> "arm" - "x86_64" -> "x86_64" - "x86" -> "x86" - else -> "" - } - } -} \ No newline at end of file diff --git a/app/src/main/java/tech/ula/utils/LocalServerManager.kt b/app/src/main/java/tech/ula/utils/LocalServerManager.kt index 50a94555d..5c9802da7 100644 --- a/app/src/main/java/tech/ula/utils/LocalServerManager.kt +++ b/app/src/main/java/tech/ula/utils/LocalServerManager.kt @@ -10,6 +10,8 @@ class LocalServerManager( private val logger: Logger = SentryLogger() ) { + private val vncDisplayNumber = 51 + fun Process.pid(): Long { return this.toString() .substringAfter("pid=") @@ -18,29 +20,6 @@ class LocalServerManager( .trim().toLong() } - private fun Session.pidRelativeFilePath(): String { - return when (this.serviceType) { - ServiceType.Ssh -> "/run/dropbear.pid" - ServiceType.Vnc -> "/home/${this.username}/.vnc/localhost:${this.port}.pid" - ServiceType.Xsdl -> "/tmp/xsdl.pidfile" - else -> "error" - } - } - - private fun Session.pidFilePath(): String { - return "$applicationFilesDirPath/${this.filesystemId}${this.pidRelativeFilePath()}" - } - - fun Session.pid(): Long { - val pidFile = File(this.pidFilePath()) - if (!pidFile.exists()) return -1 - return try { - pidFile.readText().trim().toLong() - } catch (e: Exception) { - -1 - } - } - fun startServer(session: Session): Long { return when (session.serviceType) { ServiceType.Ssh -> startSSHServer(session) @@ -50,6 +29,34 @@ class LocalServerManager( } } + fun stopService(session: Session) { + val command = "support/killProcTree.sh ${session.pid} ${session.serverPid()}" + val result = busyboxExecutor.executeScript(command) + if (result is FailedExecution) { + val details = "func: stopService err: ${result.reason}" + val breadcrumb = UlaBreadcrumb("LocalServerManager", BreadcrumbType.RuntimeError, details) + logger.addBreadcrumb(breadcrumb) + } + } + + fun isServerRunning(session: Session): Boolean { + val command = "support/isServerInProcTree.sh ${session.serverPid()}" + // The server itself is run by a third-party, so we can consider this to always be true. + // The third-party app is responsible for handling errors starting their server. + if (session.serviceType == ServiceType.Xsdl) return true + val result = busyboxExecutor.executeScript(command) + return when (result) { + is SuccessfulExecution -> true + is FailedExecution -> { + val details = "func: isServerRunning err: ${result.reason}" + val breadcrumb = UlaBreadcrumb("LocalServerManager", BreadcrumbType.RuntimeError, details) + logger.addBreadcrumb(breadcrumb) + false + } + else -> false + } + } + private fun deletePidFile(session: Session) { val pidFile = File(session.pidFilePath()) if (pidFile.exists()) pidFile.delete() @@ -123,31 +130,26 @@ class LocalServerManager( } } - fun stopService(session: Session) { - val command = "support/killProcTree.sh ${session.pid} ${session.pid()}" - val result = busyboxExecutor.executeScript(command) - if (result is FailedExecution) { - val details = "func: stopService err: ${result.reason}" - val breadcrumb = UlaBreadcrumb("LocalServerManager", BreadcrumbType.RuntimeError, details) - logger.addBreadcrumb(breadcrumb) + private fun Session.pidRelativeFilePath(): String { + return when (this.serviceType) { + ServiceType.Ssh -> "/run/dropbear.pid" + ServiceType.Vnc -> "/home/${this.username}/.vnc/localhost:$vncDisplayNumber.pid" + ServiceType.Xsdl -> "/tmp/xsdl.pidfile" + else -> "error" } } - fun isServerRunning(session: Session): Boolean { - val command = "support/isServerInProcTree.sh ${session.pid()}" - // The server itself is run by a third-party, so we can consider this to always be true. - // The third-party app is responsible for handling errors starting their server. - if (session.serviceType == ServiceType.Xsdl) return true - val result = busyboxExecutor.executeScript(command) - return when (result) { - is SuccessfulExecution -> true - is FailedExecution -> { - val details = "func: isServerRunning err: ${result.reason}" - val breadcrumb = UlaBreadcrumb("LocalServerManager", BreadcrumbType.RuntimeError, details) - logger.addBreadcrumb(breadcrumb) - false - } - else -> false + private fun Session.pidFilePath(): String { + return "$applicationFilesDirPath/${this.filesystemId}${this.pidRelativeFilePath()}" + } + + private fun Session.serverPid(): Long { + val pidFile = File(this.pidFilePath()) + if (!pidFile.exists()) return -1 + return try { + pidFile.readText().trim().toLong() + } catch (e: Exception) { + -1 } } } \ No newline at end of file diff --git a/app/src/main/java/tech/ula/utils/UlaFiles.kt b/app/src/main/java/tech/ula/utils/UlaFiles.kt index d1da0eba6..c68d6d9eb 100644 --- a/app/src/main/java/tech/ula/utils/UlaFiles.kt +++ b/app/src/main/java/tech/ula/utils/UlaFiles.kt @@ -71,6 +71,21 @@ class UlaFiles( symlinker.createSymlink(libFile.path, linkFile.path) } } + + fun getArchType(): String { + val usedABI = File(libDir, "lib_arch.so").readText() + return translateABI(usedABI) + } + + private fun translateABI(abi: String): String { + return when (abi) { + "arm64-v8a" -> "arm64" + "armeabi-v7a" -> "arm" + "x86_64" -> "x86_64" + "x86" -> "x86" + else -> "" + } + } } class Symlinker { diff --git a/app/src/main/java/tech/ula/viewmodel/AppDetailsViewModel.kt b/app/src/main/java/tech/ula/viewmodel/AppDetailsViewModel.kt index cf03f2248..90b8cb85e 100644 --- a/app/src/main/java/tech/ula/viewmodel/AppDetailsViewModel.kt +++ b/app/src/main/java/tech/ula/viewmodel/AppDetailsViewModel.kt @@ -105,7 +105,6 @@ class AppDetailsViewModel(private val sessionDao: SessionDao, private val appDet if (appSession == null) return@launch appSession.serviceType = selectedServiceType - appSession.port = if (selectedServiceType == ServiceType.Vnc) 51 else 2022 this.launch { withContext(Dispatchers.IO) { sessionDao.updateSession(appSession) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index db5183ee5..cd2332716 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -179,7 +179,6 @@ الجلسات تتطلب اسمًا فريدًا! تتطلب أنظمة الملفات اسمًا فريدًا! UserLAnd يدعم حاليا فقط جلسة واحدة نشطة! - لا يدعم UserLAnd بنية الأجهزة حاليًا. سجل تصحيح حذف أو غير موجود! diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 18ed06c21..7d385109d 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -173,7 +173,6 @@ ¡Las sesiones requieren un nombre único! ¡Los sistemas de archivos requieren un nombre único! ¡UserLAnd por ahora sólo soporta una sesión activa! - UserLAnd por ahora no soporta la arquitectura de tu dispositivo. ¡El archivo de registro de depuración fue eliminado o es inexistente! diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 218293cce..1d2acae4e 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -123,7 +123,6 @@ نام نشست‌ها باید یک‌تا باشد! نام فایل‌سیستم‌ها باید یک‌تا باشد! یوزرلند در حال حاضر تنها از یک نشست فعال پشتیبانی می‌کند! - یوزرلند در حال حاضر از معماری دستگاه شما پیشتیبانی نمی‌کند. لاگ دیباگ یا پاک شده و یا وجود ندارد! diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 3ded64cbd..ed9dfd017 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -123,7 +123,6 @@ Les Sessions ont besoin d\'un nom unique ! Les Systèmes de fichiers ont besoin d\'un nom unique ! Actuellement UserLAnd ne prend en charge qu’une seule session active ! - L’architecture de votre dispositif n’est pas actuellement prise en charge par UserLAnd. Le journal de débogage a été supprimé ou n’existe pas ! diff --git a/app/src/main/res/values-jp/strings.xml b/app/src/main/res/values-jp/strings.xml index e5d6edab8..836c2cc8d 100644 --- a/app/src/main/res/values-jp/strings.xml +++ b/app/src/main/res/values-jp/strings.xml @@ -123,7 +123,6 @@ 他のセッションと同じ名前は使用できません。 他のファイルシステムと同じ名前は使用できません。 UserLAnd は今の所1個しかセッションを立ち上げられません! - UserLAnd は現在、ご使用の端末のアーキテクチャに対応していません。 デバッグ情報のログが削除されたか、存在していません! diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index dfe13f9a0..368145241 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -200,7 +200,6 @@ Sessões exigem um nome único! Sistemas de Arquivos exigem um nome único! UserLAnd atualmente suporta apenas uma única sessão ativa! - O UserLAnd não suporta atualmente a arquitetura de seus dispositivo. Log de depuração excluído ou não existe! diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index f3bdcac0b..17070dae6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -165,7 +165,6 @@ Сессии должны иметь различные имена! Файловые системы должны иметь различные имена! На текущий момент UserLAnd поддерживает лишь единственную активную сессию! - На текущий момент UserLAnd не поддерживает архитектуру вашего устройства. Журнал отладки удалён или не существует! diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 5055fc8f4..40a086e39 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -92,7 +92,6 @@ 此会话需要一个特殊的名字! 文件系统需要一个特殊的名字! UserLAnd目前仅支持一个会话! - UserLAnd目前无法支持您的设备. 除错日志已被删除或者不存在! 图像:箭头指示会话与文件系统之间的关系 图像:分发图标 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 5fb4ea398..0d7e1df4a 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -193,7 +193,6 @@ 工作階段必須有獨一無二的名稱! 檔案系統必須有獨一無二的名稱! UserLAnd 目前僅支援單一個作用中的工作階段! - UserLAnd 目前並不支援您的裝置架構。 除錯紀錄檔已被刪除或不存在! diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 67295a159..2f32957f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -226,7 +226,6 @@ Sessions require a unique name! Filesystems require a unique name! UserLAnd currently only supports a single active session! - UserLAnd does not currently support your devices architecture. Debug log exported successfully! Exporting debug log failed! Debug log deleted! diff --git a/app/src/test/java/tech/ula/model/remote/GithubApiClientTest.kt b/app/src/test/java/tech/ula/model/remote/GithubApiClientTest.kt index 60566ec7d..518b243cc 100644 --- a/app/src/test/java/tech/ula/model/remote/GithubApiClientTest.kt +++ b/app/src/test/java/tech/ula/model/remote/GithubApiClientTest.kt @@ -16,7 +16,7 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner import tech.ula.utils.Logger -import tech.ula.utils.DeviceArchitecture +import tech.ula.utils.UlaFiles import java.io.IOException @RunWith(MockitoJUnitRunner::class) @@ -24,7 +24,7 @@ class GithubApiClientTest { @get:Rule val server = MockWebServer() - @Mock lateinit var mockDeviceArchitecture: DeviceArchitecture + @Mock lateinit var mockUlaFiles: UlaFiles @Mock lateinit var mockUrlProvider: UrlProvider @@ -76,9 +76,9 @@ class GithubApiClientTest { @Before fun setup() { - whenever(mockDeviceArchitecture.getArchType()).thenReturn(testArch) + whenever(mockUlaFiles.getArchType()).thenReturn(testArch) - githubApiClient = GithubApiClient(mockDeviceArchitecture, mockUrlProvider, mockLogger) + githubApiClient = GithubApiClient(mockUlaFiles, mockUrlProvider, mockLogger) } @After diff --git a/app/src/test/java/tech/ula/model/state/AppsStartupFsmTest.kt b/app/src/test/java/tech/ula/model/state/AppsStartupFsmTest.kt index 94f51cd00..b70ebacaa 100644 --- a/app/src/test/java/tech/ula/model/state/AppsStartupFsmTest.kt +++ b/app/src/test/java/tech/ula/model/state/AppsStartupFsmTest.kt @@ -42,7 +42,7 @@ class AppsStartupFsmTest { @Mock lateinit var mockFilesystemManager: FilesystemManager - @Mock lateinit var mockDeviceArchitecture: DeviceArchitecture + @Mock lateinit var mockUlaFiles: UlaFiles @Mock lateinit var mockLogger: Logger @@ -97,7 +97,7 @@ class AppsStartupFsmTest { whenever(mockUlaDatabase.filesystemDao()).thenReturn(mockFilesystemDao) whenever(mockUlaDatabase.sessionDao()).thenReturn(mockSessionDao) - appsFsm = AppsStartupFsm(mockUlaDatabase, mockFilesystemManager, mockDeviceArchitecture, mockLogger) + appsFsm = AppsStartupFsm(mockUlaDatabase, mockFilesystemManager, mockUlaFiles, mockLogger) } @Test @@ -165,7 +165,7 @@ class AppsStartupFsmTest { whenever(mockSessionDao.findAppsSession(app.name)) .thenReturn(listOf(appSession)) - whenever(mockDeviceArchitecture.getArchType()) + whenever(mockUlaFiles.getArchType()) .thenReturn("") whenever(mockFilesystemDao.findAppsFilesystemByType(app.filesystemRequired)) .thenReturn(listOf()) @@ -219,7 +219,7 @@ class AppsStartupFsmTest { appsFsm.setState(WaitingForAppSelection) appsFsm.getState().observeForever(mockStateObserver) - whenever(mockDeviceArchitecture.getArchType()) + whenever(mockUlaFiles.getArchType()) .thenReturn("") whenever(mockFilesystemDao.findAppsFilesystemByType(app.filesystemRequired)) .thenReturn(listOf()) diff --git a/app/src/test/java/tech/ula/utils/LocalServerManagerTest.kt b/app/src/test/java/tech/ula/utils/LocalServerManagerTest.kt index e9858d2cb..8a1f03735 100644 --- a/app/src/test/java/tech/ula/utils/LocalServerManagerTest.kt +++ b/app/src/test/java/tech/ula/utils/LocalServerManagerTest.kt @@ -42,7 +42,7 @@ class LocalServerManagerTest { private fun createVNCPidFile(session: Session) { val folder = tempFolder.newFolder(filesystemDirName, "home", session.username, ".vnc") - vncPidFile = File("${folder.path}/localhost:${session.port}.pid") + vncPidFile = File("${folder.path}/localhost:51.pid") vncPidFile.createNewFile() }