diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1ddfb..94a25f7f4 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/README.md b/README.md index 0c9cef2b7..43f14dcb3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![UserLAnd Feature Graphic](https://github.com/CypherpunkArmory/UserLAnd/raw/master/play_store/featureGraphic.png) -## Welcome to UserLAnd +# Welcome to UserLAnd The easiest way to run a Linux distribution or application on Android. Features: @@ -9,16 +9,27 @@ Features: * No root required. How to get started: -1. Define a session - This describes what filesystem you are going to use, what server you want to run (ssh or vnc), and how you want to connect to it (ConnectBot or bVNC). -2. Define a filesystem while defining a session - This describes what distro or application you want to install (only supports debian, but more coming soon). + +There are two ways to use UserLAnd: single-click apps and user-defined custom sessions. + +### Using single-click apps: +1. Click an app. +2. Fill out the required information. +3. You're good to go! + +### Using user-defined custom sessions: +1. Define a session - This describes what filesystem you are going to use, and what kind of service you want to use when connecting to it (ssh or vnc). +2. Define a filesystem - This describes what distribution of Linux you want to install. 3. Once defined, just tap on the session to start up. This will download necessary assets, setup the filesystem, start the server, and connect to it. This will take several minutes for the first start up, but will be quicker afterwards. +### Using your Linux distribution + A normal first session might look like this: -* sudo apt update <- sudo or su because you are not fake root initially, update because you need to do this +* sudo apt update <- update package information * sudo apt install wget <- install whatever you want to use * wget http://google.com <- use it -But, you can do so much more than that...your phone is not just a play thing anymore. +But you can do so much more than that. Your phone isn't just a play thing any more! This app is fully open source. You can find our code and file issues [here](https://github.com/CypherpunkArmory/UserLAnd/). diff --git a/app/build.gradle b/app/build.gradle index 09c4d1d90..adce97c1a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "tech.ula" minSdkVersion 21 targetSdkVersion 28 - versionCode 29 - versionName "0.5.2" + versionCode 32 + versionName "1.0.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -94,8 +94,6 @@ dependencies { implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation "com.android.support:design:$support_library_version" implementation "com.android.support:preference-v7:$support_library_version" - implementation "com.android.billingclient:billing:1.1" - implementation "com.google.android.gms:play-services-base:16.0.1" testImplementation 'junit:junit:4.12' testImplementation "org.mockito:mockito-core:$mockito_version" diff --git a/app/release/output.json b/app/release/output.json index 96e41edb5..a5d874b4d 100644 --- a/app/release/output.json +++ b/app/release/output.json @@ -1 +1 @@ -[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":29,"versionName":"0.5.2","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] \ No newline at end of file +[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":32,"versionName":"1.0.1","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}] \ No newline at end of file diff --git a/app/schemas/tech.ula.model.repositories.UlaDatabase/4.json b/app/schemas/tech.ula.model.repositories.UlaDatabase/4.json new file mode 100644 index 000000000..c77a9340c --- /dev/null +++ b/app/schemas/tech.ula.model.repositories.UlaDatabase/4.json @@ -0,0 +1,253 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "71f3d0d9bb252622c50dba61d887005a", + "entities": [ + { + "tableName": "session", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `filesystemId` INTEGER NOT NULL, `filesystemName` TEXT NOT NULL, `active` INTEGER NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `vncPassword` TEXT NOT NULL, `serviceType` TEXT NOT NULL, `port` INTEGER NOT NULL, `pid` INTEGER NOT NULL, `isAppsSession` INTEGER NOT NULL, FOREIGN KEY(`filesystemId`) REFERENCES `filesystem`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filesystemId", + "columnName": "filesystemId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "filesystemName", + "columnName": "filesystemName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "active", + "columnName": "active", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "vncPassword", + "columnName": "vncPassword", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serviceType", + "columnName": "serviceType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pid", + "columnName": "pid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAppsSession", + "columnName": "isAppsSession", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_session_filesystemId", + "unique": false, + "columnNames": [ + "filesystemId" + ], + "createSql": "CREATE INDEX `index_session_filesystemId` ON `${TABLE_NAME}` (`filesystemId`)" + } + ], + "foreignKeys": [ + { + "table": "filesystem", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "filesystemId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `distributionType` TEXT NOT NULL, `archType` TEXT NOT NULL, `defaultUsername` TEXT NOT NULL, `defaultPassword` TEXT NOT NULL, `defaultVncPassword` TEXT NOT NULL, `isAppsFilesystem` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "distributionType", + "columnName": "distributionType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "archType", + "columnName": "archType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultUsername", + "columnName": "defaultUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultPassword", + "columnName": "defaultPassword", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultVncPassword", + "columnName": "defaultVncPassword", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isAppsFilesystem", + "columnName": "isAppsFilesystem", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "apps", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `category` TEXT NOT NULL, `filesystemRequired` TEXT NOT NULL, `supportsCli` INTEGER NOT NULL, `supportsGui` INTEGER NOT NULL, `isPaidApp` INTEGER NOT NULL, `version` INTEGER NOT NULL, PRIMARY KEY(`name`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "filesystemRequired", + "columnName": "filesystemRequired", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "supportsCli", + "columnName": "supportsCli", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "supportsGui", + "columnName": "supportsGui", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isPaidApp", + "columnName": "isPaidApp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "name" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_apps_name", + "unique": true, + "columnNames": [ + "name" + ], + "createSql": "CREATE UNIQUE INDEX `index_apps_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + } + ], + "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, \"71f3d0d9bb252622c50dba61d887005a\")" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/tech/ula/model/MigrationTest.kt b/app/src/androidTest/java/tech/ula/model/MigrationTest.kt index 7b23628e7..383fb2708 100644 --- a/app/src/androidTest/java/tech/ula/model/MigrationTest.kt +++ b/app/src/androidTest/java/tech/ula/model/MigrationTest.kt @@ -17,6 +17,7 @@ import tech.ula.model.entities.Filesystem import tech.ula.model.entities.Session import tech.ula.model.repositories.Migration1To2 import tech.ula.model.repositories.Migration2To3 +import tech.ula.model.repositories.Migration3To4 import tech.ula.model.repositories.UlaDatabase import java.io.IOException @@ -41,16 +42,7 @@ class MigrationTest { db.close() - helper.runMigrationsAndValidate(TEST_DB, 2, true, Migration1To2()) - - val migratedDb = getMigratedDatabase() - val fs = migratedDb.filesystemDao().getFilesystemByName("firstFs") - val session = migratedDb.sessionDao().getSessionByName("firstSession") - - assertFalse(fs.isDownloaded) - assertFalse(session.isExtracted) - assert(session.lastUpdated == 0L) - assert(session.bindings == "") + helper.runMigrationsAndValidate(TEST_DB, 2, true, Migration1To2(), Migration2To3(), Migration3To4()) } @Test @@ -98,10 +90,18 @@ class MigrationTest { assertEquals(session1.vncPassword, "userland") } + @Test + @Throws(IOException::class) + fun migrate3To4() { + helper.createDatabase(TEST_DB, 3) + + helper.runMigrationsAndValidate(TEST_DB, 4, true, Migration3To4()) + } + private fun getMigratedDatabase(): UlaDatabase { val db = Room.databaseBuilder(InstrumentationRegistry.getTargetContext(), UlaDatabase::class.java, TEST_DB) - .addMigrations(Migration1To2(), Migration2To3()) + .addMigrations(Migration1To2(), Migration2To3(), Migration3To4()) .build() helper.closeWhenFinished(db) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 75d848737..76b8e3e6d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,7 +9,6 @@ - currentFragmentDisplaysProgressDialog = destination.label == getString(R.string.sessions) || - destination.label == getString(R.string.apps) + destination.label == getString(R.string.apps) || + destination.label == getString(R.string.filesystems) if (!currentFragmentDisplaysProgressDialog) killProgressBar() } @@ -156,12 +158,13 @@ class MainActivity : AppCompatActivity() { layout_progress.visibility = View.GONE layout_progress.isFocusable = false layout_progress.isClickable = false + progressBarIsVisible = false } private fun updateProgressBar(step: String, details: String) { if (!currentFragmentDisplaysProgressDialog) return - if (layout_progress.visibility != View.VISIBLE) { + if (!progressBarIsVisible) { val inAnimation = AlphaAnimation(0f, 1f) inAnimation.duration = 200 layout_progress.animation = inAnimation @@ -169,6 +172,7 @@ class MainActivity : AppCompatActivity() { layout_progress.visibility = View.VISIBLE layout_progress.isFocusable = true layout_progress.isClickable = true + progressBarIsVisible = true } text_session_list_progress_step.text = step diff --git a/app/src/main/java/tech/ula/ServerService.kt b/app/src/main/java/tech/ula/ServerService.kt index 877870428..c6eced9c3 100644 --- a/app/src/main/java/tech/ula/ServerService.kt +++ b/app/src/main/java/tech/ula/ServerService.kt @@ -47,8 +47,8 @@ class ServerService : Service() { TimestampPreferences(this.getSharedPreferences("file_timestamps", Context.MODE_PRIVATE)) } - private val assetListPreferences by lazy { - AssetListPreferences(this.getSharedPreferences("assetLists", Context.MODE_PRIVATE)) + private val assetPreferences by lazy { + AssetPreferences(this.getSharedPreferences("assetLists", Context.MODE_PRIVATE)) } private val appsList by lazy { @@ -174,19 +174,20 @@ class ServerService : Service() { lastActivatedSession = session lastActivatedFilesystem = filesystem + progressBarUpdater(getString(R.string.progress_bar_start_step), "") startForeground(NotificationUtility.serviceNotificationId, notificationManager.buildPersistentServiceNotification()) val assetRepository = AssetRepository(BuildWrapper().getArchType(), filesystem.distributionType, this.filesDir.path, timestampPreferences, - assetListPreferences) + assetPreferences) - val sessionController = SessionController(assetRepository, filesystemUtility) + val sessionController = SessionController(assetRepository, filesystemUtility, assetPreferences) launch(CommonPool) { - progressBarUpdater(resources.getString(R.string.progress_fetching_asset_lists), "") + progressBarUpdater(getString(R.string.progress_fetching_asset_lists), "") val assetLists = asyncAwait { sessionController.getAssetLists() } @@ -212,7 +213,7 @@ class ServerService : Service() { return@launch } asyncAwait { - sessionController.downloadRequirements(requiredDownloads, downloadBroadcastReceiver, + sessionController.downloadRequirements(filesystem.distributionType, requiredDownloads, downloadBroadcastReceiver, initDownloadUtility(), progressBarUpdater, resources) } @@ -257,9 +258,9 @@ class ServerService : Service() { val assetRepository = AssetRepository(BuildWrapper().getArchType(), appsFilesystemDistType, this.filesDir.path, timestampPreferences, - assetListPreferences) + assetPreferences) // TODO refactor this to not instantiate twice - val sessionController = SessionController(assetRepository, filesystemUtility) + val sessionController = SessionController(assetRepository, filesystemUtility, assetPreferences) val filesystemDao = UlaDatabase.getInstance(this).filesystemDao() val appsFilesystem = runBlocking(CommonPool) { @@ -361,6 +362,7 @@ class ServerService : Service() { private val progressBarUpdater: (String, String) -> Unit = { step: String, details: String -> + progressBarActive = true val intent = Intent(SERVER_SERVICE_RESULT) .putExtra("type", "updateProgressBar") .putExtra("step", step) diff --git a/app/src/main/java/tech/ula/model/entities/Filesystem.kt b/app/src/main/java/tech/ula/model/entities/Filesystem.kt index ba27322fb..8ac3ae149 100644 --- a/app/src/main/java/tech/ula/model/entities/Filesystem.kt +++ b/app/src/main/java/tech/ula/model/entities/Filesystem.kt @@ -4,7 +4,6 @@ import android.arch.persistence.room.Entity import android.arch.persistence.room.PrimaryKey import android.os.Parcelable import kotlinx.android.parcel.Parcelize -import java.util.Date @Parcelize @Entity(tableName = "filesystem") @@ -18,8 +17,5 @@ data class Filesystem( var defaultPassword: String = "", var defaultVncPassword: String = "", val isAppsFilesystem: Boolean = false, - val location: String = "", - val dateCreated: String = Date().toString(), - val realRoot: Boolean = false, - var isDownloaded: Boolean = false + var lastUpdated: Long = -1L ) : Parcelable \ No newline at end of file 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 f451b1dd2..11d225a53 100644 --- a/app/src/main/java/tech/ula/model/entities/Session.kt +++ b/app/src/main/java/tech/ula/model/entities/Session.kt @@ -27,16 +27,8 @@ data class Session( var username: String = "", var password: String = "userland", var vncPassword: String = "userland", - var geometry: String = "1024x768", var serviceType: String = "", - var clientType: String = "", var port: Long = 2022, var pid: Long = 0, - val startupScript: String = "", - val runAtDeviceStartup: Boolean = false, - val initialCommand: String = "", - var isExtracted: Boolean = false, - var lastUpdated: Long = 0, - var bindings: String = "", val isAppsSession: Boolean = false ) : Parcelable \ No newline at end of file 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 9dfcc3e4d..775ee6c58 100644 --- a/app/src/main/java/tech/ula/model/repositories/AssetRepository.kt +++ b/app/src/main/java/tech/ula/model/repositories/AssetRepository.kt @@ -1,7 +1,7 @@ package tech.ula.model.repositories import tech.ula.model.entities.Asset -import tech.ula.utils.AssetListPreferences +import tech.ula.utils.AssetPreferences import tech.ula.utils.ConnectionUtility import tech.ula.utils.TimestampPreferences import java.io.BufferedReader @@ -14,7 +14,7 @@ class AssetRepository( distributionType: String, private val applicationFilesDirPath: String, private val timestampPreferences: TimestampPreferences, - private val assetListPreferences: AssetListPreferences, + private val assetPreferences: AssetPreferences, private val connectionUtility: ConnectionUtility = ConnectionUtility() ) { @@ -26,7 +26,7 @@ class AssetRepository( ) fun getCachedAssetLists(): List> { - return assetListPreferences.getAssetLists(allAssetListTypes) + return assetPreferences.getAssetLists(allAssetListTypes) } fun getDistributionAssetsList(distributionType: String): List { @@ -34,7 +34,7 @@ class AssetRepository( (assetType, _) -> assetType == distributionType } - val allAssets = assetListPreferences.getAssetLists(distributionAssetLists).flatten() + val allAssets = assetPreferences.getAssetLists(distributionAssetLists).flatten() return allAssets.filter { !(it.name.contains("rootfs.tar.gz")) } } @@ -45,7 +45,7 @@ class AssetRepository( (assetType, architectureType) -> val assetList = retrieveAndParseAssetList(assetType, architectureType) allAssetLists.add(assetList) - assetListPreferences.setAssetList(assetType, architectureType, assetList) + assetPreferences.setAssetList(assetType, architectureType, assetList) } return allAssetLists.toList() } @@ -57,8 +57,9 @@ class AssetRepository( ): List { val assetList = ArrayList() + val branch = "master" val url = "$protocol://github.com/CypherpunkArmory/UserLAnd-Assets-" + - "$assetType/raw/master/assets/$architectureType/assets.txt" + "$assetType/raw/$branch/assets/$architectureType/assets.txt" if (!connectionUtility.httpsHostIsReachable("github.com")) throw object : Exception("Host is unreachable.") {} try { diff --git a/app/src/main/java/tech/ula/model/repositories/UlaDatabase.kt b/app/src/main/java/tech/ula/model/repositories/UlaDatabase.kt index 94d833fb1..acf05d38c 100644 --- a/app/src/main/java/tech/ula/model/repositories/UlaDatabase.kt +++ b/app/src/main/java/tech/ula/model/repositories/UlaDatabase.kt @@ -15,7 +15,7 @@ import tech.ula.model.daos.FilesystemDao import tech.ula.model.daos.SessionDao import tech.ula.model.entities.App -@Database(entities = [Session::class, Filesystem::class, App::class], version = 3, exportSchema = true) +@Database(entities = [Session::class, Filesystem::class, App::class], version = 4, exportSchema = true) abstract class UlaDatabase : RoomDatabase() { abstract fun sessionDao(): SessionDao @@ -36,7 +36,7 @@ abstract class UlaDatabase : RoomDatabase() { private fun buildDatabase(context: Context): UlaDatabase = Room.databaseBuilder(context.applicationContext, UlaDatabase::class.java, "Data.db") - .addMigrations(Migration1To2(), Migration2To3()) + .addMigrations(Migration1To2(), Migration2To3(), Migration3To4()) .addCallback(object : RoomDatabase.Callback() { override fun onOpen(db: SupportSQLiteDatabase) { super.onOpen(db) @@ -83,4 +83,31 @@ class Migration2To3 : Migration(2, 3) { database.execSQL("ALTER TABLE filesystem ADD COLUMN defaultVncPassword TEXT NOT NULL DEFAULT 'userland'") database.execSQL("ALTER TABLE session ADD COLUMN vncPassword TEXT NOT NULL DEFAULT 'userland'") } +} + +class Migration3To4 : Migration(3, 4) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("PRAGMA foreign_keys=off;") + database.execSQL("BEGIN TRANSACTION;") + + // Remove filesystem fields realRoot, dateCreated, location, isDownloaded. + database.execSQL("CREATE TEMPORARY TABLE filesystem_backup(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, distributionType TEXT NOT NULL, archType TEXT NOT NULL, defaultUsername TEXT NOT NULL, defaultPassword TEXT NOT NULL, defaultVncPassword TEXT NOT NULL, isAppsFilesystem INTEGER NOT NULL);") + database.execSQL("INSERT INTO filesystem_backup SELECT id, name, distributionType, archType, defaultUsername, defaultPassword, defaultVncPassword, isAppsFilesystem FROM filesystem;") + database.execSQL("DROP TABLE filesystem;") + database.execSQL("CREATE TABLE filesystem(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, distributionType TEXT NOT NULL, archType TEXT NOT NULL, defaultUsername TEXT NOT NULL, defaultPassword TEXT NOT NULL, defaultVncPassword TEXT NOT NULL, isAppsFilesystem INTEGER NOT NULL, lastUpdated INTEGER NOT NULL DEFAULT -1);") + database.execSQL("INSERT INTO filesystem SELECT id, name, distributionType, archType, defaultUsername, defaultPassword, defaultVncPassword, isAppsFilesystem, -1 FROM filesystem_backup;") + database.execSQL("DROP TABLE filesystem_backup;") + + // Remove session fields geometry, clientType, startupScript, runAtDeviceStartup, initialCommand, isExtracted, lastUpdated, bindings + database.execSQL("CREATE TEMPORARY TABLE session_backup(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, filesystemId INTEGER NOT NULL, filesystemName TEXT NOT NULL, active INTEGER NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL, vncPassword TEXT NOT NULL, serviceType TEXT NOT NULL, port INTEGER NOT NULL, pid INTEGER NOT NULL, isAppsSession INTEGER NOT NULL, FOREIGN KEY(filesystemId) REFERENCES filesystem(id))") + database.execSQL("INSERT INTO session_backup SELECT id, name, filesystemId, filesystemName, active, username, password, vncPassword, serviceType, port, pid, isAppsSession FROM session;") + database.execSQL("DROP TABLE session;") + database.execSQL("CREATE TABLE session(id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL, filesystemId INTEGER NOT NULL, filesystemName TEXT NOT NULL, active INTEGER NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL, vncPassword TEXT NOT NULL, serviceType TEXT NOT NULL, port INTEGER NOT NULL, pid INTEGER NOT NULL, isAppsSession INTEGER NOT NULL, FOREIGN KEY(filesystemId) REFERENCES filesystem(id) ON UPDATE NO ACTION ON DELETE CASCADE)") + database.execSQL("INSERT INTO session SELECT id, name, filesystemId, filesystemName, active, username, password, vncPassword, serviceType, port, pid, isAppsSession FROM session_backup;") + database.execSQL("DROP TABLE session_backup;") + database.execSQL("CREATE INDEX index_session_filesystemId ON session (filesystemId)") + + database.execSQL("COMMIT;") + database.execSQL("PRAGMA foreign_keys_on") + } } \ No newline at end of file diff --git a/app/src/main/java/tech/ula/ui/AppListAdapter.kt b/app/src/main/java/tech/ula/ui/AppListAdapter.kt index d020f9b81..1f343ab16 100644 --- a/app/src/main/java/tech/ula/ui/AppListAdapter.kt +++ b/app/src/main/java/tech/ula/ui/AppListAdapter.kt @@ -43,12 +43,10 @@ class AppListAdapter( private val ITEM_VIEW_TYPE_SEPARATOR = 1 private val firstDisplayCategory = "distribution" - private val freeAnnotation = activity.resources.getString(R.string.free_annotation) - private val paidAnnotation = activity.resources.getString(R.string.paid_annotation) private val appsAndSeparators: ArrayList = arrayListOf() - fun createAppsItemListWithSeparators(newApps: List): List { + private fun createAppsItemListWithSeparators(newApps: List): List { val listBuilder = arrayListOf() val categoriesAndApps = HashMap>() @@ -68,9 +66,7 @@ class AppListAdapter( categoriesAndAppsWithDistributionsFirst.forEach { (category, categoryApps) -> - val categoryWithPaymentInformation = category + " " + - if (category == firstDisplayCategory) freeAnnotation else paidAnnotation - listBuilder.add(AppSeparatorItem(categoryWithPaymentInformation.capitalize())) + listBuilder.add(AppSeparatorItem(category.capitalize())) val sortedCategoryApps = categoryApps.sortedWith(compareBy { it.name }) sortedCategoryApps.forEach { listBuilder.add(AppItem(it)) } diff --git a/app/src/main/java/tech/ula/ui/AppListFragment.kt b/app/src/main/java/tech/ula/ui/AppListFragment.kt index 1d7bdbdc6..c6ea3fc6e 100644 --- a/app/src/main/java/tech/ula/ui/AppListFragment.kt +++ b/app/src/main/java/tech/ula/ui/AppListFragment.kt @@ -40,15 +40,12 @@ import tech.ula.utils.AppsPreferences import tech.ula.utils.DefaultPreferences import tech.ula.utils.ExecUtility import tech.ula.utils.FilesystemUtility -import tech.ula.utils.PlayServiceManager import tech.ula.utils.ValidationUtility import tech.ula.utils.arePermissionsGranted -import tech.ula.utils.displayGenericErrorDialog import tech.ula.viewmodel.AppListViewModel import tech.ula.viewmodel.AppListViewModelFactory class AppListFragment : Fragment(), - PlayServiceManager.PlayServicesUpdateListener, AppListAdapter.OnAppsItemClicked, AppListAdapter.OnAppsCreateContextMenu { @@ -67,11 +64,6 @@ class AppListFragment : Fragment(), private val unselectedApp = App(name = "unselected") private var lastSelectedApp = unselectedApp - private var billingClientIsConnected = false - private val playServiceManager by lazy { - PlayServiceManager(this) - } - private lateinit var filesystemList: List private val execUtility by lazy { @@ -105,7 +97,7 @@ class AppListFragment : Fragment(), appsList = it.first activeSessions = it.second appAdapter.updateAppsAndSessions(appsList, activeSessions) - if (appsList.isEmpty()) { + if (appsList.isEmpty() || userlandIsNewVersion()) { doRefresh() } } @@ -169,10 +161,6 @@ class AppListFragment : Fragment(), appsListViewModel.getRefreshStatus().observe(viewLifecycleOwner, refreshStatusObserver) appsListViewModel.getAllFilesystems().observe(viewLifecycleOwner, filesystemObserver) - if (playServiceManager.playServicesAreAvailable(activityContext)) { - playServiceManager.startBillingClient(activityContext) - } - registerForContextMenu(list_apps) list_apps.layoutManager = LinearLayoutManager(list_apps.context) list_apps.adapter = appAdapter @@ -187,6 +175,7 @@ class AppListFragment : Fragment(), private fun doRefresh() { appsListViewModel.refreshAppsList() + setLatestUpdateUserlandVersion() } private fun doAppItemClicked(selectedApp: App) { @@ -200,24 +189,6 @@ class AppListFragment : Fragment(), private fun handleAppSelection(selectedApp: App) { if (selectedApp == unselectedApp) return - if (selectedApp.isPaidApp) { - when { - !playServiceManager.playStoreIsAvailable(activityContext.packageManager) -> { - displayGenericErrorDialog(activityContext, R.string.general_error_title, - R.string.alert_play_store_required) - return - } - !playServiceManager.playServicesAreAvailable(activityContext) -> { - displayGenericErrorDialog(activityContext, R.string.general_error_title, - R.string.alert_play_service_error_message) - return - } - !playServiceManager.userHasYearlyAppsSubscription() -> { - playServiceManager.startBillingFlow(activityContext) - return - } - } - } val preferredServiceType = appsListViewModel.getAppServiceTypePreference(selectedApp).toLowerCase() @@ -477,34 +448,22 @@ class AppListFragment : Fragment(), .create().show() } - override fun onSubscriptionsAreNotSupported() { - AlertDialog.Builder(activityContext) - .setMessage(R.string.alert_subscriptions_unsupported_message) - .setTitle(R.string.general_error_title) - .setPositiveButton(R.string.button_ok) { - dialog, _ -> - dialog.dismiss() - } - .create().show() - } - - override fun onBillingClientConnectionChange(isConnected: Boolean) { - billingClientIsConnected = isConnected - if (!isConnected) playServiceManager.startBillingClient(activityContext) + private fun userlandIsNewVersion(): Boolean { + val version = getUserlandVersion() + val lastUpdatedVersion = activityContext.defaultSharedPreferences.getString("lastAppsUpdate", "") + return version != lastUpdatedVersion } - override fun onPlayServiceError() { - AlertDialog.Builder(activityContext) - .setMessage(R.string.alert_play_service_error_message) - .setTitle(R.string.general_error_title) - .setPositiveButton(R.string.button_ok) { - dialog, _ -> - dialog.dismiss() - } - .create().show() + private fun setLatestUpdateUserlandVersion() { + val version = getUserlandVersion() + with(activityContext.defaultSharedPreferences.edit()) { + putString("lastAppsUpdate", version) + apply() + } } - override fun onSubscriptionPurchased() { - handleAppSelection(lastSelectedApp) + private fun getUserlandVersion(): String { + val info = activityContext.packageManager.getPackageInfo(activityContext.packageName, 0) + return info.versionName } } \ No newline at end of file diff --git a/app/src/main/java/tech/ula/ui/SessionEditFragment.kt b/app/src/main/java/tech/ula/ui/SessionEditFragment.kt index 6be43ebde..8f2382d18 100644 --- a/app/src/main/java/tech/ula/ui/SessionEditFragment.kt +++ b/app/src/main/java/tech/ula/ui/SessionEditFragment.kt @@ -122,7 +122,6 @@ class SessionEditFragment : Fragment() { } "" -> return else -> { - // TODO adapter to associate filesystem structure with list items? val filesystem = filesystemList.find { it.name == filesystemName } filesystem?.let { updateFilesystemDetailsForSession(it) diff --git a/app/src/main/java/tech/ula/utils/AndroidUtility.kt b/app/src/main/java/tech/ula/utils/AndroidUtility.kt index 2e25725b9..274d5724e 100644 --- a/app/src/main/java/tech/ula/utils/AndroidUtility.kt +++ b/app/src/main/java/tech/ula/utils/AndroidUtility.kt @@ -86,7 +86,7 @@ class TimestampPreferences(private val prefs: SharedPreferences) { } } -class AssetListPreferences(private val prefs: SharedPreferences) { +class AssetPreferences(private val prefs: SharedPreferences) { fun getAssetLists(allAssetListTypes: List>): List> { val assetLists = ArrayList>() allAssetListTypes.forEach { @@ -110,6 +110,17 @@ class AssetListPreferences(private val prefs: SharedPreferences) { apply() } } + + fun getLastDistributionUpdate(distributionType: String): Long { + return prefs.getLong("$distributionType-lastUpdate", -1) + } + + fun setLastDistributionUpdate(distributionType: String, currentTimeMillis: Long) { + with(prefs.edit()) { + putLong("$distributionType-lastUpdate", currentTimeMillis) + apply() + } + } } class AppsPreferences(private val prefs: SharedPreferences) { @@ -289,4 +300,10 @@ class LocalFileLocator(private val applicationFilesDir: String, private val reso } return appDescriptionFile.readText() } +} + +class TimeUtility { + fun getCurrentTimeMillis(): Long { + return System.currentTimeMillis() + } } \ No newline at end of file diff --git a/app/src/main/java/tech/ula/utils/DownloadUtility.kt b/app/src/main/java/tech/ula/utils/DownloadUtility.kt index fff9c4cd0..1367fddff 100644 --- a/app/src/main/java/tech/ula/utils/DownloadUtility.kt +++ b/app/src/main/java/tech/ula/utils/DownloadUtility.kt @@ -18,8 +18,9 @@ class DownloadUtility( } private fun download(asset: Asset): Long { + val branch = "master" val url = "https://github.com/CypherpunkArmory/UserLAnd-Assets-" + - "${asset.distributionType}/raw/master/assets/" + + "${asset.distributionType}/raw/$branch/assets/" + "${asset.architectureType}/${asset.name}" val destination = asset.concatenatedName val request = downloadManagerWrapper.generateDownloadRequest(url, destination) diff --git a/app/src/main/java/tech/ula/utils/PlayServiceManager.kt b/app/src/main/java/tech/ula/utils/PlayServiceManager.kt deleted file mode 100644 index ee332facf..000000000 --- a/app/src/main/java/tech/ula/utils/PlayServiceManager.kt +++ /dev/null @@ -1,118 +0,0 @@ -package tech.ula.utils - -import android.app.Activity -import android.content.pm.PackageManager -import com.android.billingclient.api.BillingClient -import com.android.billingclient.api.PurchasesUpdatedListener -import com.android.billingclient.api.BillingClient.BillingResponse -import com.android.billingclient.api.BillingClientStateListener -import com.android.billingclient.api.BillingFlowParams -import com.android.billingclient.api.Purchase -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability -import tech.ula.BuildConfig - -class PlayServiceManager(private val playServicesUpdateListener: PlayServicesUpdateListener) : PurchasesUpdatedListener { - - private lateinit var billingClient: BillingClient - - private val appsYearlySubId = "apps_yearly_subscription" - private val packageName = "tech.ula" - private val playServicesResolutionRequest = 9000 // Arbitrary - - interface PlayServicesUpdateListener { - fun onSubscriptionsAreNotSupported() - fun onBillingClientConnectionChange(isConnected: Boolean) - fun onSubscriptionPurchased() - fun onPlayServiceError() - } - - override fun onPurchasesUpdated(responseCode: Int, purchases: MutableList?) { - if (responseCode == BillingResponse.OK) { - purchases?.forEach { - if (purchaseIsForYearlyAppsSub(it)) { - playServicesUpdateListener.onSubscriptionPurchased() - } - } - } else if (responseCode == BillingResponse.USER_CANCELED) return - else playServicesUpdateListener.onPlayServiceError() - } - - fun playStoreIsAvailable(packageManager: PackageManager): Boolean { - if (!BuildConfig.ENABLE_PLAY_SERVICES) return true - val playStorePackageName = "com.android.vending" - return try { - packageManager.getPackageInfo(playStorePackageName, 0) - true - } catch (err: PackageManager.NameNotFoundException) { - false - } - } - - fun playServicesAreAvailable(activity: Activity): Boolean { - if (!BuildConfig.ENABLE_PLAY_SERVICES) return true - val googleApi = GoogleApiAvailability.getInstance() - val result = googleApi.isGooglePlayServicesAvailable(activity) - if (result != ConnectionResult.SUCCESS) { - if (googleApi.isUserResolvableError(result)) { - googleApi.getErrorDialog(activity, result, playServicesResolutionRequest).show() - } - return false - } - return true - } - - fun startBillingClient(activity: Activity) { - if (!BuildConfig.ENABLE_PLAY_SERVICES) return - billingClient = BillingClient.newBuilder(activity).setListener(this).build() - billingClient.startConnection(object : BillingClientStateListener { - override fun onBillingSetupFinished(responseCode: Int) { - if (responseCode == BillingClient.BillingResponse.OK) { - playServicesUpdateListener.onBillingClientConnectionChange(isConnected = true) - } - } - - override fun onBillingServiceDisconnected() { - playServicesUpdateListener.onBillingClientConnectionChange(isConnected = false) - } - }) - } - - fun startBillingFlow(activity: Activity) { - if (!BuildConfig.ENABLE_PLAY_SERVICES) { - playServicesUpdateListener.onSubscriptionPurchased() - return - } - if (!subscriptionsAreSupported()) { - playServicesUpdateListener.onSubscriptionsAreNotSupported() - } - val flowParams = BillingFlowParams.newBuilder() - .setSku(appsYearlySubId) - .setType(BillingClient.SkuType.SUBS) - .build() - billingClient.launchBillingFlow(activity, flowParams) - } - - fun userHasYearlyAppsSubscription(): Boolean { - if (!BuildConfig.ENABLE_PLAY_SERVICES) return true // Always have subscriptions if debugging - return cacheIndicatesUserHasPurchasedSubscription() - } - - private fun cacheIndicatesUserHasPurchasedSubscription(): Boolean { - // Purchases list is nullable even though not documented as such. Null *at least* when play - // store is unavailable - val purchasesList = billingClient.queryPurchases(BillingClient.SkuType.SUBS).purchasesList - return purchasesList?.any { - purchaseIsForYearlyAppsSub(it) - } ?: false - } - - private fun purchaseIsForYearlyAppsSub(purchase: Purchase): Boolean { - return purchase.sku == appsYearlySubId && purchase.packageName == packageName - } - - private fun subscriptionsAreSupported(): Boolean { - val responseCode = billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS) - return responseCode == BillingResponse.OK - } -} \ No newline at end of file diff --git a/app/src/main/java/tech/ula/utils/SessionController.kt b/app/src/main/java/tech/ula/utils/SessionController.kt index b7ca126ae..1ff50f492 100644 --- a/app/src/main/java/tech/ula/utils/SessionController.kt +++ b/app/src/main/java/tech/ula/utils/SessionController.kt @@ -13,7 +13,9 @@ import tech.ula.model.repositories.AssetRepository class SessionController( private val assetRepository: AssetRepository, - private val filesystemUtility: FilesystemUtility + private val filesystemUtility: FilesystemUtility, + private val assetPreferences: AssetPreferences, + private val timeUtility: TimeUtility = TimeUtility() ) { @Throws // If device architecture is unsupported @@ -50,10 +52,9 @@ class SessionController( val portOrDisplay: Long = if (serviceType == "ssh") 2022 else 51 if (potentialAppSession.isEmpty()) { - val clientType = if (serviceType == "ssh") "ConnectBot" else "bVNC" // TODO update clients dynamically somehow val sessionToInsert = Session(id = 0, name = appName, filesystemId = appsFilesystem.id, filesystemName = appsFilesystem.name, serviceType = serviceType, - clientType = clientType, username = appsFilesystem.defaultUsername, + username = appsFilesystem.defaultUsername, password = appsFilesystem.defaultPassword, vncPassword = appsFilesystem.defaultVncPassword, isAppsSession = true, port = portOrDisplay) asyncAwait { sessionDao.insertSession(sessionToInsert) } @@ -167,6 +168,7 @@ class SessionController( } suspend fun downloadRequirements( + distributionType: String, requiredDownloads: List, downloadBroadcastReceiver: DownloadBroadcastReceiver, downloadUtility: DownloadUtility, @@ -188,6 +190,7 @@ class SessionController( progressBarUpdater(resources.getString(R.string.progress_copying_downloads), "") downloadUtility.moveAssetsToCorrectLocalDirectory() + assetPreferences.setLastDistributionUpdate(distributionType, timeUtility.getCurrentTimeMillis()) } // Return value represents successful extraction. Also true if extraction is unnecessary. @@ -210,7 +213,10 @@ class SessionController( fun ensureFilesystemHasRequiredAssets(filesystem: Filesystem) { val filesystemDirectoryName = "${filesystem.id}" val requiredDistributionAssets = assetRepository.getDistributionAssetsList(filesystem.distributionType) - if (!filesystemUtility.areAllRequiredAssetsPresent(filesystemDirectoryName, requiredDistributionAssets)) { + val filesystemNeedsUpdating = filesystem.lastUpdated < + assetPreferences.getLastDistributionUpdate(filesystem.distributionType) + if (filesystemNeedsUpdating || !filesystemUtility.areAllRequiredAssetsPresent( + filesystemDirectoryName, requiredDistributionAssets)) { filesystemUtility.copyDistributionAssetsToFilesystem(filesystemDirectoryName, filesystem.distributionType) filesystemUtility.removeRootfsFilesFromFilesystem(filesystemDirectoryName) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 115e02f0d..edb22945d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,6 +58,7 @@ UserLAnd is running a background service. + Getting things ready for you... Setting up app database information… Fetching asset lists… Checking whether updates are required… diff --git a/app/src/test/java/tech/ula/model/repositories/AssetRepositoryTest.kt b/app/src/test/java/tech/ula/model/repositories/AssetRepositoryTest.kt index d32575863..d32fb6094 100644 --- a/app/src/test/java/tech/ula/model/repositories/AssetRepositoryTest.kt +++ b/app/src/test/java/tech/ula/model/repositories/AssetRepositoryTest.kt @@ -14,7 +14,7 @@ import org.mockito.Mockito.`when` import org.mockito.Mockito.times import org.mockito.junit.MockitoJUnitRunner import tech.ula.model.entities.Asset -import tech.ula.utils.AssetListPreferences +import tech.ula.utils.AssetPreferences import tech.ula.utils.ConnectionUtility import tech.ula.utils.TimestampPreferences import java.io.ByteArrayInputStream @@ -32,7 +32,7 @@ class AssetRepositoryTest { lateinit var timestampPreferences: TimestampPreferences @Mock - lateinit var assetListPreferences: AssetListPreferences + lateinit var assetPreferences: AssetPreferences @Mock lateinit var connectionUtility: ConnectionUtility @@ -54,13 +54,13 @@ class AssetRepositoryTest { fun setup() { applicationFilesDirPath = tempFolder.root.path assetRepository = AssetRepository(archType, distType, applicationFilesDirPath, - timestampPreferences, assetListPreferences, connectionUtility) + timestampPreferences, assetPreferences, connectionUtility) } @Test fun allTypesOfCachedAssetListsAreRetrieved() { assetRepository.getCachedAssetLists() - verify(assetListPreferences).getAssetLists(allAssetListTypes) + verify(assetPreferences).getAssetLists(allAssetListTypes) } @Test @@ -72,7 +72,7 @@ class AssetRepositoryTest { val asset1 = Asset("name", distType, archType, 0) val asset2 = Asset("rootfs.tar.gz", distType, archType, 0) val assetListWithRootfsFile = listOf(listOf(asset1, asset2)) - `when`(assetListPreferences.getAssetLists(distTypeAssetLists)).thenReturn(assetListWithRootfsFile) + `when`(assetPreferences.getAssetLists(distTypeAssetLists)).thenReturn(assetListWithRootfsFile) val returnedAssetList = assetRepository.getDistributionAssetsList(distType) diff --git a/app/src/test/java/tech/ula/utils/SessionControllerTest.kt b/app/src/test/java/tech/ula/utils/SessionControllerTest.kt index e9c2f5afd..b42d6ae59 100644 --- a/app/src/test/java/tech/ula/utils/SessionControllerTest.kt +++ b/app/src/test/java/tech/ula/utils/SessionControllerTest.kt @@ -24,7 +24,7 @@ class SessionControllerTest { // Class dependencies - val testFilesystem = Filesystem(name = "testFS", id = 1) + val testFilesystem = Filesystem(name = "testFS", id = 1, lastUpdated = 0) @Mock lateinit var buildWrapper: BuildWrapper @@ -35,6 +35,12 @@ class SessionControllerTest { @Mock lateinit var filesystemUtility: FilesystemUtility + @Mock + lateinit var assetPreferences: AssetPreferences + + @Mock + lateinit var timeUtility: TimeUtility + @Mock lateinit var networkUtility: NetworkUtility @@ -48,7 +54,7 @@ class SessionControllerTest { @Before fun setup() { - sessionController = SessionController(assetRepository, filesystemUtility) + sessionController = SessionController(assetRepository, filesystemUtility, assetPreferences, timeUtility) } @Test @@ -84,7 +90,7 @@ class SessionControllerTest { val appName = "testApp" val serviceType = "ssh" val appSession = Session(0, name = appName, filesystemId = 0, filesystemName = "apps", - serviceType = serviceType, username = "username", clientType = "ConnectBot", isAppsSession = true) + serviceType = serviceType, username = "username", isAppsSession = true) whenever(sessionDao.findAppsSession(appName)) .thenReturn(listOf()) @@ -191,6 +197,7 @@ class SessionControllerTest { val filesystemDirectoryName = "${testFilesystem.id}" `when`(assetRepository.getDistributionAssetsList(testFilesystem.distributionType)) .thenReturn(distAssetList) + whenever(assetPreferences.getLastDistributionUpdate(testFilesystem.distributionType)).thenReturn(-1) `when`(filesystemUtility.areAllRequiredAssetsPresent(filesystemDirectoryName, distAssetList)) .thenReturn(false) @@ -199,4 +206,18 @@ class SessionControllerTest { verify(filesystemUtility).copyDistributionAssetsToFilesystem(filesystemDirectoryName, testFilesystem.distributionType) verify(filesystemUtility).removeRootfsFilesFromFilesystem(filesystemDirectoryName) } + + @Test + fun copiesDistributionAssetsToFilesystemIfTheyAreOutdated() { + val distAssetList = listOf(Asset("name", "dist", "arch", 0)) + val filesystemDirectoryName = "${testFilesystem.id}" + `when`(assetRepository.getDistributionAssetsList(testFilesystem.distributionType)) + .thenReturn(distAssetList) + whenever(assetPreferences.getLastDistributionUpdate(testFilesystem.distributionType)).thenReturn(1) + + sessionController.ensureFilesystemHasRequiredAssets(testFilesystem) + + verify(filesystemUtility).copyDistributionAssetsToFilesystem(filesystemDirectoryName, testFilesystem.distributionType) + verify(filesystemUtility).removeRootfsFilesFromFilesystem(filesystemDirectoryName) + } } \ No newline at end of file