refactor: split into app-store (server) and app-store-android
Move the Kotlin Android client into its own repository and drop the helloworld sample. This repo keeps the FastAPI distribution server with its web frontend.
This commit is contained in:
parent
aa8bba2825
commit
bd2e0e3413
@ -1,85 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'com.android.application'
|
|
||||||
id 'org.jetbrains.kotlin.android'
|
|
||||||
id 'com.google.devtools.ksp'
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace 'com.localstore'
|
|
||||||
compileSdk 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.localstore"
|
|
||||||
minSdk 26
|
|
||||||
targetSdk 34
|
|
||||||
versionCode 1
|
|
||||||
versionName "1.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
signingConfigs {
|
|
||||||
release {
|
|
||||||
storeFile file("${System.getenv('HOME')}/playground/app-store/signing/localstore.keystore")
|
|
||||||
storePassword "localstore123"
|
|
||||||
keyAlias "localstore"
|
|
||||||
keyPassword "localstore123"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
signingConfig signingConfigs.release
|
|
||||||
}
|
|
||||||
debug {
|
|
||||||
signingConfig signingConfigs.debug
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
viewBinding true
|
|
||||||
}
|
|
||||||
|
|
||||||
lint {
|
|
||||||
checkReleaseBuilds false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'androidx.core:core-ktx:1.13.1'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
|
||||||
implementation 'com.google.android.material:material:1.12.0'
|
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
|
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.3.2'
|
|
||||||
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
|
|
||||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
|
|
||||||
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4'
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.8.4'
|
|
||||||
|
|
||||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.8.0'
|
|
||||||
implementation 'androidx.navigation:navigation-ui-ktx:2.8.0'
|
|
||||||
|
|
||||||
implementation 'com.squareup.retrofit2:retrofit:2.11.0'
|
|
||||||
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
|
|
||||||
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
|
|
||||||
|
|
||||||
implementation 'io.coil-kt:coil:2.6.0'
|
|
||||||
|
|
||||||
implementation 'androidx.room:room-runtime:2.6.1'
|
|
||||||
implementation 'androidx.room:room-ktx:2.6.1'
|
|
||||||
ksp 'androidx.room:room-compiler:2.6.1'
|
|
||||||
|
|
||||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
|
|
||||||
|
|
||||||
implementation 'androidx.datastore:datastore-preferences:1.1.1'
|
|
||||||
}
|
|
||||||
@ -1,32 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:name=".LocalStoreApp"
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:networkSecurityConfig="@xml/network_security_config"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/Theme.LocalAppStore"
|
|
||||||
tools:targetApi="31">
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".ui.MainActivity"
|
|
||||||
android:exported="true"
|
|
||||||
android:theme="@style/Theme.LocalAppStore">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
package com.localstore
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import com.localstore.data.AppDatabase
|
|
||||||
|
|
||||||
class LocalStoreApp : Application() {
|
|
||||||
|
|
||||||
val database: AppDatabase by lazy {
|
|
||||||
AppDatabase.getInstance(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
package com.localstore.data
|
|
||||||
|
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import retrofit2.http.Body
|
|
||||||
import retrofit2.http.GET
|
|
||||||
import retrofit2.http.POST
|
|
||||||
import retrofit2.http.Path
|
|
||||||
import retrofit2.http.Query
|
|
||||||
|
|
||||||
interface AppApi {
|
|
||||||
@GET("api/apps")
|
|
||||||
suspend fun getApps(
|
|
||||||
@Query("search") search: String? = null,
|
|
||||||
@Query("offset") offset: Int = 0,
|
|
||||||
@Query("limit") limit: Int = 50
|
|
||||||
): AppListResponse
|
|
||||||
|
|
||||||
@GET("api/apps/{appId}")
|
|
||||||
suspend fun getApp(@Path("appId") appId: String): App
|
|
||||||
|
|
||||||
@GET("api/apps/{appId}/download")
|
|
||||||
suspend fun downloadApp(@Path("appId") appId: String): ResponseBody
|
|
||||||
|
|
||||||
@GET("api/apps/{appId}/icon")
|
|
||||||
suspend fun getAppIcon(@Path("appId") appId: String): ResponseBody
|
|
||||||
|
|
||||||
@POST("api/apps/update-check")
|
|
||||||
suspend fun checkForUpdate(@Body request: UpdateCheckRequest): UpdateCheckResponse
|
|
||||||
|
|
||||||
@POST("api/scan")
|
|
||||||
suspend fun triggerScan(): ScanResult
|
|
||||||
|
|
||||||
@GET("api/status")
|
|
||||||
suspend fun getServerStatus(): ServerStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ScanResult(
|
|
||||||
val scanned: Boolean,
|
|
||||||
val appsCount: Int
|
|
||||||
)
|
|
||||||
@ -1,186 +0,0 @@
|
|||||||
package com.localstore.data
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.room.Dao
|
|
||||||
import androidx.room.Database
|
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Entity
|
|
||||||
import androidx.room.Insert
|
|
||||||
import androidx.room.OnConflictStrategy
|
|
||||||
import androidx.room.PrimaryKey
|
|
||||||
import androidx.room.Query
|
|
||||||
import androidx.room.Room
|
|
||||||
import androidx.room.RoomDatabase
|
|
||||||
import androidx.room.TypeConverter
|
|
||||||
import androidx.room.TypeConverters
|
|
||||||
import androidx.room.Update
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.reflect.TypeToken
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
@Entity(tableName = "apps")
|
|
||||||
data class AppEntity(
|
|
||||||
@PrimaryKey val id: String,
|
|
||||||
val packageName: String,
|
|
||||||
val versionName: String,
|
|
||||||
val versionCode: String,
|
|
||||||
val name: String,
|
|
||||||
val description: String,
|
|
||||||
val icon: String?,
|
|
||||||
val size: Long,
|
|
||||||
val screenshotsJson: String = "[]",
|
|
||||||
val minSdk: String?,
|
|
||||||
val targetSdk: String?,
|
|
||||||
val permissionsJson: String = "[]"
|
|
||||||
)
|
|
||||||
|
|
||||||
data class App(
|
|
||||||
val id: String,
|
|
||||||
val packageName: String,
|
|
||||||
val versionName: String,
|
|
||||||
val versionCode: String,
|
|
||||||
val name: String,
|
|
||||||
val description: String,
|
|
||||||
val icon: String?,
|
|
||||||
val size: Long,
|
|
||||||
val screenshots: List<String> = emptyList(),
|
|
||||||
val minSdk: String?,
|
|
||||||
val targetSdk: String?,
|
|
||||||
val permissions: List<String> = emptyList()
|
|
||||||
)
|
|
||||||
|
|
||||||
fun App.toEntity(): AppEntity {
|
|
||||||
return AppEntity(
|
|
||||||
id = id,
|
|
||||||
packageName = packageName,
|
|
||||||
versionName = versionName,
|
|
||||||
versionCode = versionCode,
|
|
||||||
name = name,
|
|
||||||
description = description,
|
|
||||||
icon = icon,
|
|
||||||
size = size,
|
|
||||||
screenshotsJson = Gson().toJson(screenshots),
|
|
||||||
minSdk = minSdk,
|
|
||||||
targetSdk = targetSdk,
|
|
||||||
permissionsJson = Gson().toJson(permissions)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun AppEntity.toApp(): App {
|
|
||||||
return App(
|
|
||||||
id = id,
|
|
||||||
packageName = packageName,
|
|
||||||
versionName = versionName,
|
|
||||||
versionCode = versionCode,
|
|
||||||
name = name,
|
|
||||||
description = description,
|
|
||||||
icon = icon,
|
|
||||||
size = size,
|
|
||||||
screenshots = try {
|
|
||||||
val type = object : TypeToken<List<String>>() {}.type
|
|
||||||
Gson().fromJson<List<String>>(screenshotsJson, type) ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
emptyList()
|
|
||||||
},
|
|
||||||
minSdk = minSdk,
|
|
||||||
targetSdk = targetSdk,
|
|
||||||
permissions = try {
|
|
||||||
val type = object : TypeToken<List<String>>() {}.type
|
|
||||||
Gson().fromJson<List<String>>(permissionsJson, type) ?: emptyList()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class AppListResponse(
|
|
||||||
val apps: List<App>,
|
|
||||||
val total: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
data class UpdateCheckRequest(
|
|
||||||
val packageName: String,
|
|
||||||
val currentVersionCode: String
|
|
||||||
)
|
|
||||||
|
|
||||||
data class UpdateCheckResponse(
|
|
||||||
val hasUpdate: Boolean,
|
|
||||||
val app: App?,
|
|
||||||
val currentVersionCode: String,
|
|
||||||
val latestVersionCode: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
data class ServerStatus(
|
|
||||||
val server: String,
|
|
||||||
val version: String,
|
|
||||||
val appsCount: Int
|
|
||||||
)
|
|
||||||
|
|
||||||
class AppTypeConverter {
|
|
||||||
private val gson = Gson()
|
|
||||||
|
|
||||||
@TypeConverter
|
|
||||||
fun fromStringList(value: String?): List<String> {
|
|
||||||
if (value.isNullOrEmpty()) return emptyList()
|
|
||||||
return try {
|
|
||||||
val type = object : TypeToken<List<String>>() {}.type
|
|
||||||
gson.fromJson<List<String>>(value, type)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@TypeConverter
|
|
||||||
fun toStringList(list: List<String>): String {
|
|
||||||
return gson.toJson(list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Dao
|
|
||||||
@TypeConverters(AppTypeConverter::class)
|
|
||||||
interface AppDao {
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
suspend fun insertApps(apps: List<AppEntity>)
|
|
||||||
|
|
||||||
@Query("SELECT * FROM apps ORDER BY name ASC")
|
|
||||||
fun getAllApps(): Flow<List<AppEntity>>
|
|
||||||
|
|
||||||
@Query("SELECT * FROM apps WHERE id = :id")
|
|
||||||
suspend fun getAppById(id: String): AppEntity?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM apps WHERE packageName = :packageName")
|
|
||||||
suspend fun getAppByPackage(packageName: String): AppEntity?
|
|
||||||
|
|
||||||
@Query("SELECT * FROM apps WHERE name LIKE :query OR description LIKE :query ORDER BY name ASC")
|
|
||||||
fun searchApps(query: String): Flow<List<AppEntity>>
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
suspend fun deleteApp(app: AppEntity)
|
|
||||||
|
|
||||||
@Query("DELETE FROM apps")
|
|
||||||
suspend fun clearAll()
|
|
||||||
|
|
||||||
@Update
|
|
||||||
suspend fun updateApp(app: AppEntity)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Database(entities = [AppEntity::class], version = 1, exportSchema = false)
|
|
||||||
@TypeConverters(AppTypeConverter::class)
|
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
|
||||||
abstract fun appDao(): AppDao
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@Volatile
|
|
||||||
private var INSTANCE: AppDatabase? = null
|
|
||||||
|
|
||||||
fun getInstance(context: Context): AppDatabase {
|
|
||||||
return INSTANCE ?: synchronized(this) {
|
|
||||||
INSTANCE ?: Room.databaseBuilder(
|
|
||||||
context.applicationContext,
|
|
||||||
AppDatabase::class.java,
|
|
||||||
"app_store_database"
|
|
||||||
).build().also { INSTANCE = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
package com.localstore.data
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.localstore.LocalStoreApp
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import okhttp3.logging.HttpLoggingInterceptor
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class AppRepository(
|
|
||||||
private val context: Context,
|
|
||||||
defaultServerUrl: String = "http://192.168.8.128:9800"
|
|
||||||
) {
|
|
||||||
private val appDao = (context.applicationContext as LocalStoreApp).database.appDao()
|
|
||||||
@Volatile private var serverUrl = defaultServerUrl
|
|
||||||
@Volatile private var api: AppApi = createApi(defaultServerUrl)
|
|
||||||
|
|
||||||
private fun createApi(url: String): AppApi {
|
|
||||||
val logging = HttpLoggingInterceptor().apply {
|
|
||||||
level = HttpLoggingInterceptor.Level.BODY
|
|
||||||
}
|
|
||||||
val client = OkHttpClient.Builder()
|
|
||||||
.addInterceptor(logging)
|
|
||||||
.connectTimeout(10, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
return Retrofit.Builder()
|
|
||||||
.baseUrl("$url/")
|
|
||||||
.client(client)
|
|
||||||
.addConverterFactory(GsonConverterFactory.create())
|
|
||||||
.build()
|
|
||||||
.create(AppApi::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setServerUrl(url: String) {
|
|
||||||
serverUrl = url
|
|
||||||
api = createApi(url)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getAppsFlow(): Flow<List<App>> = appDao.getAllApps().map { entities ->
|
|
||||||
entities.map { it.toApp() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun searchAppsFlow(query: String): Flow<List<App>> =
|
|
||||||
appDao.searchApps("%$query%").map { entities ->
|
|
||||||
entities.map { it.toApp() }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun refreshApps() {
|
|
||||||
val response = api.getApps()
|
|
||||||
appDao.clearAll()
|
|
||||||
appDao.insertApps(response.apps.map { it.toEntity() })
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getAppById(id: String): App? {
|
|
||||||
val cached = appDao.getAppById(id)?.toApp()
|
|
||||||
if (cached != null) return cached
|
|
||||||
return runCatching {
|
|
||||||
val response = api.getApp(id)
|
|
||||||
val entity = response.toEntity()
|
|
||||||
appDao.insertApps(listOf(entity))
|
|
||||||
response
|
|
||||||
}.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getAppByPackage(packageName: String): App? {
|
|
||||||
return appDao.getAppByPackage(packageName)?.toApp()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun checkForUpdate(packageName: String, currentVersionCode: String): UpdateCheckResponse {
|
|
||||||
return api.checkForUpdate(UpdateCheckRequest(packageName, currentVersionCode))
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun downloadApp(id: String): ResponseBody {
|
|
||||||
return api.downloadApp(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun getServerStatus(): ServerStatus {
|
|
||||||
return api.getServerStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
package com.localstore.data
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.datastore.preferences.core.Preferences
|
|
||||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
|
||||||
import androidx.datastore.preferences.preferencesDataStore
|
|
||||||
import androidx.datastore.preferences.core.edit
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import retrofit2.Retrofit
|
|
||||||
|
|
||||||
object PreferencesKeys {
|
|
||||||
val SERVER_ADDRESS = stringPreferencesKey("server_address")
|
|
||||||
val SERVER_PORT = stringPreferencesKey("server_port")
|
|
||||||
}
|
|
||||||
|
|
||||||
private val Context.dataStore by preferencesDataStore("settings")
|
|
||||||
|
|
||||||
class SettingsManager(private val context: Context) {
|
|
||||||
|
|
||||||
fun getServerAddressFlow(): Flow<String> =
|
|
||||||
context.dataStore.data
|
|
||||||
.catch { throw it }
|
|
||||||
.map { prefs ->
|
|
||||||
prefs[PreferencesKeys.SERVER_ADDRESS] ?: "192.168.8.128"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getServerPortFlow(): Flow<String> =
|
|
||||||
context.dataStore.data
|
|
||||||
.catch { throw it }
|
|
||||||
.map { prefs ->
|
|
||||||
prefs[PreferencesKeys.SERVER_PORT] ?: "8080"
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getServerUrlFlow(): Flow<String> =
|
|
||||||
context.dataStore.data
|
|
||||||
.catch { throw it }
|
|
||||||
.map { prefs ->
|
|
||||||
val address = prefs[PreferencesKeys.SERVER_ADDRESS] ?: "192.168.8.128"
|
|
||||||
val port = prefs[PreferencesKeys.SERVER_PORT] ?: "9800"
|
|
||||||
"http://$address:$port"
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun setServerAddress(address: String) {
|
|
||||||
context.dataStore.edit { prefs ->
|
|
||||||
prefs[PreferencesKeys.SERVER_ADDRESS] = address
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun setServerPort(port: String) {
|
|
||||||
context.dataStore.edit { prefs ->
|
|
||||||
prefs[PreferencesKeys.SERVER_PORT] = port
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun testConnection(): Result<ServerStatus> {
|
|
||||||
val address = context.dataStore.data.catch { throw it }
|
|
||||||
.map { prefs -> prefs[PreferencesKeys.SERVER_ADDRESS] ?: "192.168.8.128" }
|
|
||||||
.first()
|
|
||||||
val port = context.dataStore.data.catch { throw it }
|
|
||||||
.map { prefs -> prefs[PreferencesKeys.SERVER_PORT] ?: "9800" }
|
|
||||||
.first()
|
|
||||||
|
|
||||||
return runCatching {
|
|
||||||
val client = okhttp3.OkHttpClient.Builder()
|
|
||||||
.connectTimeout(5, java.util.concurrent.TimeUnit.SECONDS)
|
|
||||||
.readTimeout(10, java.util.concurrent.TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
val retrofit = Retrofit.Builder()
|
|
||||||
.baseUrl("http://$address:$port/")
|
|
||||||
.client(client)
|
|
||||||
.addConverterFactory(retrofit2.converter.gson.GsonConverterFactory.create())
|
|
||||||
.build()
|
|
||||||
val api = retrofit.create(AppApi::class.java)
|
|
||||||
api.getServerStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
package com.localstore.installer
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.PackageInstaller
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.provider.Settings
|
|
||||||
import android.util.Log
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
class AppInstaller(private val context: Context) {
|
|
||||||
|
|
||||||
private val packageManager: PackageManager = context.packageManager
|
|
||||||
|
|
||||||
fun isAppInstalled(packageName: String): Boolean {
|
|
||||||
return try {
|
|
||||||
packageManager.getPackageInfo(packageName, 0)
|
|
||||||
true
|
|
||||||
} catch (e: Exception) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getInstalledPackages(): Set<String> {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
packageManager.getInstalledPackages(PackageManager.MATCH_ALL)
|
|
||||||
.map { it.packageName }
|
|
||||||
.toSet()
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
packageManager.getInstalledPackages(0)
|
|
||||||
.map { it.packageName }
|
|
||||||
.toSet()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPackageInfo(packageName: String): android.content.pm.PackageInfo? {
|
|
||||||
return try {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
packageManager.getPackageInfo(packageName, PackageManager.MATCH_ALL)
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
packageManager.getPackageInfo(packageName, 0)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun canInstall(): Boolean {
|
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
packageManager.canRequestPackageInstalls()
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openInstallPermissionSettings() {
|
|
||||||
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
intent.data = Uri.parse("package:${context.packageName}")
|
|
||||||
context.startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun prepareInstall(inputStream: InputStream): File {
|
|
||||||
val tempFile = File(context.cacheDir, "download.apk")
|
|
||||||
tempFile.outputStream().use { outputStream ->
|
|
||||||
inputStream.copyTo(outputStream)
|
|
||||||
}
|
|
||||||
return tempFile
|
|
||||||
}
|
|
||||||
|
|
||||||
fun install(apkFile: File): Boolean {
|
|
||||||
if (!canInstall()) {
|
|
||||||
Log.e("AppInstaller", "Install permission not granted")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val packageInstaller = packageManager.packageInstaller
|
|
||||||
val sessionParams = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
|
||||||
val sessionId = packageInstaller.createSession(sessionParams)
|
|
||||||
|
|
||||||
packageInstaller.openSession(sessionId).use { session ->
|
|
||||||
val fis = java.io.FileInputStream(apkFile)
|
|
||||||
val buffer = ByteArray(65536)
|
|
||||||
var bytes: Int
|
|
||||||
session.openWrite("base", 0, -1).use { outputStream ->
|
|
||||||
while (fis.read(buffer).also { bytes = it } > 0) {
|
|
||||||
outputStream.write(buffer, 0, bytes)
|
|
||||||
session.fsync(outputStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fis.close()
|
|
||||||
|
|
||||||
val intent = Intent(context, com.localstore.ui.MainActivity::class.java)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
|
||||||
val pendingIntent = android.app.PendingIntent.getActivity(
|
|
||||||
context, 0, intent,
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
|
|
||||||
android.app.PendingIntent.FLAG_IMMUTABLE or android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
else
|
|
||||||
android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
|
||||||
)
|
|
||||||
session.commit(pendingIntent.intentSender)
|
|
||||||
}
|
|
||||||
|
|
||||||
apkFile.delete()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
fun installViaIntent(apkFile: File): Intent {
|
|
||||||
val intent = Intent(Intent.ACTION_INSTALL_PACKAGE)
|
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
intent.data = Uri.fromFile(apkFile)
|
|
||||||
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
|
||||||
return intent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
package com.localstore.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.navigation.findNavController
|
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
|
||||||
import androidx.navigation.ui.setupWithNavController
|
|
||||||
import com.localstore.R
|
|
||||||
import com.localstore.databinding.ActivityMainBinding
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
lateinit var binding: ActivityMainBinding
|
|
||||||
lateinit var navController: androidx.navigation.NavController
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
val navHostFragment = NavHostFragment.create(R.navigation.nav_graph)
|
|
||||||
supportFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.nav_host_container, navHostFragment)
|
|
||||||
.commitAllowingStateLoss()
|
|
||||||
|
|
||||||
navController = navHostFragment.navController
|
|
||||||
binding.bottomNav.setupWithNavController(navController)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,168 +0,0 @@
|
|||||||
package com.localstore.ui.details
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import coil.load
|
|
||||||
import com.localstore.R
|
|
||||||
import com.localstore.data.App
|
|
||||||
import com.localstore.databinding.FragmentDetailsBinding
|
|
||||||
import com.localstore.installer.AppInstaller
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
|
|
||||||
class DetailsFragment : Fragment() {
|
|
||||||
|
|
||||||
private var _binding: FragmentDetailsBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val viewModel: DetailsViewModel by viewModels {
|
|
||||||
androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val installer = AppInstaller(requireContext())
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentDetailsBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
val appId = arguments?.getString(ARG_APP_ID) ?: run {
|
|
||||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.loadApp(appId)
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.app.collect { app ->
|
|
||||||
app?.let { bindApp(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.installState.collect { state ->
|
|
||||||
binding.installButton.apply {
|
|
||||||
when (state) {
|
|
||||||
is InstallState.Idle -> {
|
|
||||||
text = if (viewModel.isInstalled.value) requireContext().getString(R.string.installed) else requireContext().getString(R.string.install)
|
|
||||||
isEnabled = true
|
|
||||||
binding.progressBar.visibility = View.GONE
|
|
||||||
}
|
|
||||||
is InstallState.Downloading -> {
|
|
||||||
text = requireContext().getString(R.string.downloading)
|
|
||||||
isEnabled = false
|
|
||||||
binding.progressBar.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
is InstallState.Installing -> {
|
|
||||||
text = requireContext().getString(R.string.installing)
|
|
||||||
isEnabled = false
|
|
||||||
binding.progressBar.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
is InstallState.Success -> {
|
|
||||||
text = requireContext().getString(R.string.installed)
|
|
||||||
isEnabled = false
|
|
||||||
binding.progressBar.visibility = View.GONE
|
|
||||||
Toast.makeText(requireContext(), requireContext().getString(R.string.install_success), Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
is InstallState.Failed -> {
|
|
||||||
text = requireContext().getString(R.string.install)
|
|
||||||
isEnabled = true
|
|
||||||
binding.progressBar.visibility = View.GONE
|
|
||||||
Toast.makeText(requireContext(), state.message, Toast.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.isInstalled.collect { installed ->
|
|
||||||
if (!installed && viewModel.installState.value is InstallState.Idle) {
|
|
||||||
binding.installButton.text = requireContext().getString(R.string.install)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.installButton.setOnClickListener {
|
|
||||||
if (!installer.canInstall()) {
|
|
||||||
Toast.makeText(requireContext(), R.string.grant_install_permission, Toast.LENGTH_LONG).show()
|
|
||||||
installer.openInstallPermissionSettings()
|
|
||||||
return@setOnClickListener
|
|
||||||
}
|
|
||||||
viewModel.installApp()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.backButton.setOnClickListener {
|
|
||||||
requireActivity().onBackPressedDispatcher.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindApp(app: App) {
|
|
||||||
val serverHost = viewModel.serverUrl.value
|
|
||||||
val host = serverHost.replace("http://", "").split(":").first()
|
|
||||||
|
|
||||||
binding.apply {
|
|
||||||
appName.text = app.name
|
|
||||||
appVersion.text = "Version: ${app.versionName}"
|
|
||||||
appPackage.text = app.packageName
|
|
||||||
|
|
||||||
val sizeStr = when {
|
|
||||||
app.size > 1_000_000 -> String.format("%.1f MB", app.size / 1_000_000.0)
|
|
||||||
app.size > 1_000 -> String.format("%.1f KB", app.size / 1_000.0)
|
|
||||||
else -> "${app.size} B"
|
|
||||||
}
|
|
||||||
appSize.text = "Size: $sizeStr"
|
|
||||||
|
|
||||||
appDescription.text = if (app.description.isNotBlank()) app.description else "No description available"
|
|
||||||
|
|
||||||
if (!app.icon.isNullOrBlank()) {
|
|
||||||
appIcon.load("http://$host:9800/api/apps/${app.id}/icon") {
|
|
||||||
crossfade(true)
|
|
||||||
placeholder(android.R.drawable.ic_dialog_info)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
appIcon.setImageResource(android.R.drawable.ic_dialog_info)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app.permissions.isNotEmpty()) {
|
|
||||||
appPermissionsContainer.visibility = View.VISIBLE
|
|
||||||
val permissionsText = app.permissions.joinToString("\n") { "• $it" }
|
|
||||||
appPermissionsList.text = permissionsText
|
|
||||||
} else {
|
|
||||||
appPermissionsContainer.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val ARG_APP_ID = "app_id"
|
|
||||||
|
|
||||||
fun newInstance(appId: String): DetailsFragment {
|
|
||||||
return DetailsFragment().apply {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putString(ARG_APP_ID, appId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
package com.localstore.ui.details
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.localstore.data.App
|
|
||||||
import com.localstore.data.AppRepository
|
|
||||||
import com.localstore.data.SettingsManager
|
|
||||||
import com.localstore.installer.AppInstaller
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
sealed class InstallState {
|
|
||||||
object Idle : InstallState()
|
|
||||||
object Downloading : InstallState()
|
|
||||||
object Installing : InstallState()
|
|
||||||
data class Success(val app: App) : InstallState()
|
|
||||||
data class Failed(val message: String) : InstallState()
|
|
||||||
}
|
|
||||||
|
|
||||||
class DetailsViewModel(
|
|
||||||
private val context: Context
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val settingsManager = SettingsManager(context)
|
|
||||||
private val repository = AppRepository(context)
|
|
||||||
private val installer = AppInstaller(context)
|
|
||||||
|
|
||||||
val serverUrl = MutableStateFlow("http://192.168.8.128:9800")
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
settingsManager.getServerUrlFlow().collect { url ->
|
|
||||||
serverUrl.value = url
|
|
||||||
repository.setServerUrl(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val app = MutableStateFlow<App?>(null)
|
|
||||||
val installState = MutableStateFlow<InstallState>(InstallState.Idle)
|
|
||||||
val downloadProgress = MutableStateFlow(0)
|
|
||||||
val isInstalled = MutableStateFlow(false)
|
|
||||||
|
|
||||||
fun loadApp(appId: String) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
val loadedApp = repository.getAppById(appId)
|
|
||||||
app.value = loadedApp
|
|
||||||
isInstalled.value = installer.isAppInstalled(loadedApp?.packageName ?: "")
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun installApp() {
|
|
||||||
val currentApp = app.value ?: return
|
|
||||||
installState.value = InstallState.Downloading
|
|
||||||
downloadProgress.value = 0
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
val responseBody = repository.downloadApp(currentApp.id)
|
|
||||||
val tempFile = installer.prepareInstall(responseBody.byteStream())
|
|
||||||
|
|
||||||
installState.value = InstallState.Installing
|
|
||||||
val result = installer.install(tempFile)
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
installState.value = InstallState.Success(currentApp)
|
|
||||||
isInstalled.value = true
|
|
||||||
} else {
|
|
||||||
installState.value = InstallState.Failed("Installation failed")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
installState.value = InstallState.Failed(e.message ?: "Unknown error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun resetInstallState() {
|
|
||||||
installState.value = InstallState.Idle
|
|
||||||
downloadProgress.value = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,71 +0,0 @@
|
|||||||
package com.localstore.ui.home
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import coil.load
|
|
||||||
import com.localstore.data.App
|
|
||||||
import com.localstore.databinding.ItemAppBinding
|
|
||||||
|
|
||||||
class AppsAdapter(
|
|
||||||
private val onItemClick: (App) -> Unit
|
|
||||||
) : ListAdapter<App, AppsAdapter.AppViewHolder>(AppDiffCallback()) {
|
|
||||||
|
|
||||||
var serverHost: String = "192.168.8.128"
|
|
||||||
var serverPort: Int = 9800
|
|
||||||
var contextRef: Context? = null
|
|
||||||
|
|
||||||
fun setServerInfo(host: String, port: Int, ctx: Context) {
|
|
||||||
serverHost = host
|
|
||||||
serverPort = port
|
|
||||||
contextRef = ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder {
|
|
||||||
val binding = ItemAppBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
return AppViewHolder(binding, serverHost, serverPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
|
|
||||||
holder.bind(getItem(position), onItemClick)
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppViewHolder(
|
|
||||||
private val binding: ItemAppBinding,
|
|
||||||
private val serverHost: String,
|
|
||||||
private val serverPort: Int
|
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun bind(app: App, onItemClick: (App) -> Unit) {
|
|
||||||
binding.apply {
|
|
||||||
appName.text = app.name
|
|
||||||
appVersion.text = app.versionName
|
|
||||||
|
|
||||||
appIcon.apply {
|
|
||||||
if (!app.icon.isNullOrBlank()) {
|
|
||||||
load("http://$serverHost:$serverPort/api/apps/${app.id}/icon") {
|
|
||||||
crossfade(true)
|
|
||||||
placeholder(android.R.drawable.ic_dialog_info)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setImageResource(android.R.drawable.ic_dialog_info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
root.setOnClickListener { onItemClick(app) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppDiffCallback : DiffUtil.ItemCallback<App>() {
|
|
||||||
override fun areItemsTheSame(oldItem: App, newItem: App) = oldItem.id == newItem.id
|
|
||||||
override fun areContentsTheSame(oldItem: App, newItem: App) = oldItem == newItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,107 +0,0 @@
|
|||||||
package com.localstore.ui.home
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import com.localstore.data.App
|
|
||||||
import com.localstore.databinding.FragmentHomeBinding
|
|
||||||
import com.localstore.ui.MainActivity
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class HomeFragment : Fragment() {
|
|
||||||
|
|
||||||
private var _binding: FragmentHomeBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val viewModel: HomeViewModel by viewModels {
|
|
||||||
androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var adapter: AppsAdapter
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
adapter = AppsAdapter { app ->
|
|
||||||
(requireActivity() as MainActivity).navController
|
|
||||||
.navigate(com.localstore.R.id.nav_details, Bundle().apply { putString("app_id", app.id) })
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.recyclerView.apply {
|
|
||||||
layoutManager = GridLayoutManager(requireContext(), 3)
|
|
||||||
adapter = this@HomeFragment.adapter
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.searchEditText.addTextChangedListener(object : android.text.TextWatcher {
|
|
||||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
|
||||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
|
||||||
viewModel.setSearchQuery(s.toString())
|
|
||||||
}
|
|
||||||
override fun afterTextChanged(s: android.text.Editable?) {}
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.swipeRefresh.setOnRefreshListener {
|
|
||||||
viewModel.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.apps.collect { apps ->
|
|
||||||
adapter.submitList(apps)
|
|
||||||
binding.swipeRefresh.isRefreshing = false
|
|
||||||
updateEmptyState(apps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.searchResults.collect { apps ->
|
|
||||||
if (binding.searchEditText.text.isNotEmpty()) {
|
|
||||||
adapter.submitList(apps)
|
|
||||||
updateEmptyState(apps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.isLoading.collect { isLoading ->
|
|
||||||
binding.swipeRefresh.isRefreshing = isLoading
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.error.collect { error ->
|
|
||||||
error?.let {
|
|
||||||
Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show()
|
|
||||||
binding.swipeRefresh.isRefreshing = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateEmptyState(apps: List<App>) {
|
|
||||||
binding.emptyState.visibility = if (apps.isEmpty()) View.VISIBLE else View.GONE
|
|
||||||
binding.recyclerView.visibility = if (apps.isEmpty()) View.GONE else View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
package com.localstore.ui.home
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.localstore.data.App
|
|
||||||
import com.localstore.data.AppRepository
|
|
||||||
import com.localstore.data.SettingsManager
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class HomeViewModel(
|
|
||||||
private val context: Context
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val settingsManager = SettingsManager(context)
|
|
||||||
private val repository = AppRepository(context)
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
settingsManager.getServerUrlFlow().collect { url ->
|
|
||||||
repository.setServerUrl(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val apps = repository.getAppsFlow()
|
|
||||||
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
|
||||||
|
|
||||||
val searchQuery = MutableStateFlow("")
|
|
||||||
val searchResults = searchQuery
|
|
||||||
.combine(repository.getAppsFlow()) { query, allApps ->
|
|
||||||
if (query.isBlank()) allApps
|
|
||||||
else allApps.filter {
|
|
||||||
it.name.lowercase().contains(query.lowercase()) ||
|
|
||||||
it.description.lowercase().contains(query.lowercase())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
|
|
||||||
|
|
||||||
val isLoading = MutableStateFlow(false)
|
|
||||||
val error = MutableStateFlow<String?>(null)
|
|
||||||
|
|
||||||
fun refresh() {
|
|
||||||
isLoading.value = true
|
|
||||||
error.value = null
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
repository.refreshApps()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
error.value = e.message ?: "Refresh failed"
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSearchQuery(query: String) {
|
|
||||||
searchQuery.value = query
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
package com.localstore.ui.myapps
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import coil.load
|
|
||||||
import com.localstore.databinding.ItemInstalledAppBinding
|
|
||||||
|
|
||||||
class InstalledAppsAdapter(
|
|
||||||
private val onItemClick: (InstalledApp) -> Unit
|
|
||||||
) : ListAdapter<InstalledApp, InstalledAppsAdapter.AppViewHolder>(InstalledAppDiffCallback()) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppViewHolder {
|
|
||||||
val binding = ItemInstalledAppBinding.inflate(
|
|
||||||
LayoutInflater.from(parent.context),
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
return AppViewHolder(binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: AppViewHolder, position: Int) {
|
|
||||||
holder.bind(getItem(position), onItemClick)
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppViewHolder(
|
|
||||||
private val binding: ItemInstalledAppBinding
|
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun bind(app: InstalledApp, onItemClick: (InstalledApp) -> Unit) {
|
|
||||||
binding.apply {
|
|
||||||
appName.text = app.app.name
|
|
||||||
appVersion.text = "v${app.installedVersionName}"
|
|
||||||
|
|
||||||
if (!app.app.icon.isNullOrBlank()) {
|
|
||||||
appIcon.load("http://192.168.8.128:9800/api/apps/${app.app.id}/icon") {
|
|
||||||
crossfade(true)
|
|
||||||
placeholder(android.R.drawable.ic_dialog_info)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
appIcon.setImageResource(android.R.drawable.ic_dialog_info)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBadge.visibility = if (app.hasUpdate) View.VISIBLE else View.GONE
|
|
||||||
|
|
||||||
root.setOnClickListener { onItemClick(app) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class InstalledAppDiffCallback : DiffUtil.ItemCallback<InstalledApp>() {
|
|
||||||
override fun areItemsTheSame(oldItem: InstalledApp, newItem: InstalledApp) =
|
|
||||||
oldItem.app.id == newItem.app.id
|
|
||||||
override fun areContentsTheSame(oldItem: InstalledApp, newItem: InstalledApp) =
|
|
||||||
oldItem == newItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
package com.localstore.ui.myapps
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.localstore.databinding.FragmentMyAppsBinding
|
|
||||||
import com.localstore.ui.MainActivity
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class MyAppsFragment : Fragment() {
|
|
||||||
|
|
||||||
private var _binding: FragmentMyAppsBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val viewModel: MyAppsViewModel by viewModels {
|
|
||||||
androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var adapter: InstalledAppsAdapter
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentMyAppsBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
adapter = InstalledAppsAdapter { app ->
|
|
||||||
(requireActivity() as MainActivity).navController
|
|
||||||
.navigate(com.localstore.R.id.nav_details, Bundle().apply { putString("app_id", app.app.id) })
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.recyclerView.apply {
|
|
||||||
layoutManager = LinearLayoutManager(requireContext())
|
|
||||||
adapter = this@MyAppsFragment.adapter
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.checkUpdatesButton.setOnClickListener {
|
|
||||||
viewModel.checkForUpdates()
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.installedApps.collect { apps ->
|
|
||||||
adapter.submitList(apps)
|
|
||||||
updateEmptyState(apps)
|
|
||||||
|
|
||||||
val updateCount = apps.count { it.hasUpdate }
|
|
||||||
if (updateCount > 0) {
|
|
||||||
binding.updatesBadge.visibility = View.VISIBLE
|
|
||||||
binding.updatesBadge.text = updateCount.toString()
|
|
||||||
} else {
|
|
||||||
binding.updatesBadge.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.isLoading.collect { isLoading ->
|
|
||||||
binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE
|
|
||||||
binding.checkUpdatesButton.isEnabled = !isLoading
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.loadInstalledApps()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateEmptyState(apps: List<InstalledApp>) {
|
|
||||||
binding.emptyState.visibility = if (apps.isEmpty()) View.VISIBLE else View.GONE
|
|
||||||
binding.recyclerView.visibility = if (apps.isEmpty()) View.GONE else View.VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
viewModel.loadInstalledApps()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,89 +0,0 @@
|
|||||||
package com.localstore.ui.myapps
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageInfo
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.localstore.data.App
|
|
||||||
import com.localstore.data.AppRepository
|
|
||||||
import com.localstore.data.SettingsManager
|
|
||||||
import com.localstore.installer.AppInstaller
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
data class InstalledApp(
|
|
||||||
val app: App,
|
|
||||||
val installedVersionCode: Int,
|
|
||||||
val installedVersionName: String,
|
|
||||||
val hasUpdate: Boolean = false
|
|
||||||
)
|
|
||||||
|
|
||||||
class MyAppsViewModel(
|
|
||||||
private val context: Context
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val settingsManager = SettingsManager(context)
|
|
||||||
private val repository = AppRepository(context)
|
|
||||||
private val installer = AppInstaller(context)
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
settingsManager.getServerUrlFlow().collect { url ->
|
|
||||||
repository.setServerUrl(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val installedApps = MutableStateFlow<List<InstalledApp>>(emptyList())
|
|
||||||
val isLoading = MutableStateFlow(false)
|
|
||||||
|
|
||||||
fun loadInstalledApps() {
|
|
||||||
isLoading.value = true
|
|
||||||
viewModelScope.launch {
|
|
||||||
try {
|
|
||||||
val repoApps = repository.getAppsFlow().first()
|
|
||||||
val installed = installer.getInstalledPackages()
|
|
||||||
|
|
||||||
val installedAppsList = repoApps.filter { repoApp ->
|
|
||||||
installed.contains(repoApp.packageName)
|
|
||||||
}.map { repoApp ->
|
|
||||||
val pkgInfo = installer.getPackageInfo(repoApp.packageName)
|
|
||||||
InstalledApp(
|
|
||||||
app = repoApp,
|
|
||||||
installedVersionCode = pkgInfo?.longVersionCode?.toInt() ?: 0,
|
|
||||||
installedVersionName = pkgInfo?.versionName ?: "unknown",
|
|
||||||
hasUpdate = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
installedApps.value = installedAppsList
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun checkForUpdates() {
|
|
||||||
isLoading.value = true
|
|
||||||
viewModelScope.launch {
|
|
||||||
val currentApps = installedApps.value.toMutableList()
|
|
||||||
|
|
||||||
for ((index, installedApp) in currentApps.withIndex()) {
|
|
||||||
try {
|
|
||||||
val response = repository.checkForUpdate(
|
|
||||||
installedApp.app.packageName,
|
|
||||||
installedApp.installedVersionCode.toString()
|
|
||||||
)
|
|
||||||
currentApps[index] = installedApp.copy(hasUpdate = response.hasUpdate)
|
|
||||||
installedApps.value = currentApps
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,124 +0,0 @@
|
|||||||
package com.localstore.ui.settings
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.localstore.R
|
|
||||||
import com.localstore.databinding.FragmentSettingsBinding
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class SettingsFragment : Fragment() {
|
|
||||||
|
|
||||||
private var _binding: FragmentSettingsBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val viewModel: SettingsViewModel by viewModels {
|
|
||||||
androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.serverAddress.collect { address ->
|
|
||||||
binding.serverAddressEditText.setText(address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.serverPort.collect { port ->
|
|
||||||
binding.serverPortEditText.setText(port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewModel.testResult.collect { result ->
|
|
||||||
when (result) {
|
|
||||||
is SettingsViewModel.TestResult.Idle -> {
|
|
||||||
binding.testButton.isEnabled = true
|
|
||||||
binding.testResultText.visibility = View.GONE
|
|
||||||
}
|
|
||||||
is SettingsViewModel.TestResult.Testing -> {
|
|
||||||
binding.testButton.isEnabled = false
|
|
||||||
binding.testResultText.visibility = View.VISIBLE
|
|
||||||
binding.testResultText.text = "${requireContext().getString(R.string.connection_test)}..."
|
|
||||||
binding.testResultText.setTextColor(requireContext().getColor(android.R.color.darker_gray))
|
|
||||||
}
|
|
||||||
is SettingsViewModel.TestResult.Success -> {
|
|
||||||
binding.testButton.isEnabled = true
|
|
||||||
binding.testResultText.visibility = View.VISIBLE
|
|
||||||
binding.testResultText.text = String.format(
|
|
||||||
requireContext().getString(R.string.connection_success),
|
|
||||||
result.status.appsCount
|
|
||||||
)
|
|
||||||
binding.testResultText.setTextColor(requireContext().getColor(R.color.primary))
|
|
||||||
}
|
|
||||||
is SettingsViewModel.TestResult.Failed -> {
|
|
||||||
binding.testButton.isEnabled = true
|
|
||||||
binding.testResultText.visibility = View.VISIBLE
|
|
||||||
binding.testResultText.text = result.message
|
|
||||||
binding.testResultText.setTextColor(requireContext().getColor(R.color.error))
|
|
||||||
}
|
|
||||||
null -> {
|
|
||||||
binding.testButton.isEnabled = true
|
|
||||||
binding.testResultText.visibility = View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.saveButton.setOnClickListener {
|
|
||||||
val address = binding.serverAddressEditText.text.toString().trim()
|
|
||||||
val port = binding.serverPortEditText.text.toString().trim()
|
|
||||||
|
|
||||||
if (address.isEmpty()) {
|
|
||||||
Toast.makeText(requireContext(), "Server address is required", Toast.LENGTH_SHORT).show()
|
|
||||||
return@setOnClickListener
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.setServerAddress(address)
|
|
||||||
if (port.isNotEmpty()) {
|
|
||||||
viewModel.setServerPort(port)
|
|
||||||
}
|
|
||||||
|
|
||||||
Toast.makeText(requireContext(), "Settings saved", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.testButton.setOnClickListener {
|
|
||||||
val address = binding.serverAddressEditText.text.toString().trim()
|
|
||||||
val port = binding.serverPortEditText.text.toString().trim()
|
|
||||||
|
|
||||||
if (address.isEmpty()) {
|
|
||||||
Toast.makeText(requireContext(), "Enter server address first", Toast.LENGTH_SHORT).show()
|
|
||||||
return@setOnClickListener
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.setServerAddress(address)
|
|
||||||
if (port.isNotEmpty()) {
|
|
||||||
viewModel.setServerPort(port)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.testConnection()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
package com.localstore.ui.settings
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.localstore.data.AppApi
|
|
||||||
import com.localstore.data.ServerStatus
|
|
||||||
import com.localstore.data.SettingsManager
|
|
||||||
import kotlinx.coroutines.flow.*
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
class SettingsViewModel(
|
|
||||||
private val context: Context
|
|
||||||
) : ViewModel() {
|
|
||||||
|
|
||||||
private val settingsManager = SettingsManager(context)
|
|
||||||
|
|
||||||
val serverAddress = settingsManager.getServerAddressFlow()
|
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, "192.168.8.128")
|
|
||||||
|
|
||||||
val serverPort = settingsManager.getServerPortFlow()
|
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, "9800")
|
|
||||||
|
|
||||||
val serverUrl = settingsManager.getServerUrlFlow()
|
|
||||||
.stateIn(viewModelScope, SharingStarted.Lazily, "")
|
|
||||||
|
|
||||||
val testResult = MutableStateFlow<TestResult?>(null)
|
|
||||||
|
|
||||||
sealed class TestResult {
|
|
||||||
object Idle : TestResult()
|
|
||||||
object Testing : TestResult()
|
|
||||||
data class Success(val status: ServerStatus) : TestResult()
|
|
||||||
data class Failed(val message: String) : TestResult()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setServerAddress(address: String) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
settingsManager.setServerAddress(address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setServerPort(port: String) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
settingsManager.setServerPort(port)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun testConnection() {
|
|
||||||
testResult.value = TestResult.Testing
|
|
||||||
viewModelScope.launch {
|
|
||||||
val result = settingsManager.testConnection()
|
|
||||||
testResult.value = if (result.isSuccess) {
|
|
||||||
TestResult.Success(result.getOrNull()!!)
|
|
||||||
} else {
|
|
||||||
TestResult.Failed(result.exceptionOrNull()?.message ?: "Connection failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="rectangle">
|
|
||||||
<corners android:radius="12dp" />
|
|
||||||
<solid android:color="@color/accent" />
|
|
||||||
</shape>
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M60,54 L54,48 L54,60 L66,60 Z" />
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M36,30 L72,30 L72,36 L36,36 Z" />
|
|
||||||
<path
|
|
||||||
android:pathData="M36,36 L36,78 L72,78 L72,36"
|
|
||||||
android:strokeWidth="4"
|
|
||||||
android:strokeColor="#FFFFFF" />
|
|
||||||
</vector>
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/nav_host_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<com.google.android.material.bottomnavigation.BottomNavigationView
|
|
||||||
android:id="@+id/bottom_nav"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:menu="@menu/bottom_nav_menu" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
||||||
@ -1,160 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/backButton"
|
|
||||||
android:layout_width="40dp"
|
|
||||||
android:layout_height="40dp"
|
|
||||||
android:src="@android:drawable/ic_menu_revert"
|
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"
|
|
||||||
android:contentDescription="Back" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/appIcon"
|
|
||||||
android:layout_width="80dp"
|
|
||||||
android:layout_height="80dp"
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:scaleType="centerInside"
|
|
||||||
android:contentDescription="App icon" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/appName"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="12dp"
|
|
||||||
android:textSize="22sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textColor="?android:textColorPrimary" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center"
|
|
||||||
android:layout_marginTop="4dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/appVersion"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textColor="?android:textColorSecondary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/appSize"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="16dp"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textColor="?android:textColorSecondary" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/appPackage"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textColor="?android:textColorSecondary" />
|
|
||||||
|
|
||||||
<com.google.android.material.divider.MaterialDivider
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_marginBottom="16dp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/description"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="?android:textColorPrimary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/appDescription"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:lineSpacingExtra="4dp"
|
|
||||||
android:textColor="?android:textColorSecondary" />
|
|
||||||
|
|
||||||
<com.google.android.material.divider.MaterialDivider
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="16dp"
|
|
||||||
android:layout_marginBottom="16dp" />
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/appPermissionsContainer"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/permissions"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="?android:textColorPrimary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/appPermissionsList"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="24dp"
|
|
||||||
android:textSize="13sp"
|
|
||||||
android:lineSpacingExtra="4dp"
|
|
||||||
android:textColor="?android:textColorSecondary" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:background="?android:windowBackground">
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/progressBar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/installButton"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="56dp"
|
|
||||||
android:text="@string/install"
|
|
||||||
app:cornerRadius="28dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
||||||
@ -1,62 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<com.google.android.material.search.SearchBar
|
|
||||||
android:id="@+id/searchBar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="8dp">
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/searchEditText"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/search_apps"
|
|
||||||
android:inputType="text"
|
|
||||||
android:background="@null"
|
|
||||||
android:padding="8dp" />
|
|
||||||
|
|
||||||
</com.google.android.material.search.SearchBar>
|
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
|
||||||
android:id="@+id/swipeRefresh"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1">
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recyclerView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:padding="8dp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/emptyState"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:text="@string/no_apps_found"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:textColor="?android:textColorSecondary"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:padding="16dp"
|
|
||||||
android:gravity="center_vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:text="@string/nav_my_apps"
|
|
||||||
android:textSize="20sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="?android:textColorPrimary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/updatesBadge"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/badge_background"
|
|
||||||
android:padding="6dp"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/checkUpdatesButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/check_for_updates"
|
|
||||||
style="@style/Widget.Material3.Button.TextButton" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/progressBar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1">
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/recyclerView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:padding="8dp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/emptyState"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:text="@string/no_installed_apps"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:textColor="?android:textColorSecondary"
|
|
||||||
android:gravity="center"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/server_settings"
|
|
||||||
android:textSize="20sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="?android:textColorPrimary"
|
|
||||||
android:layout_marginBottom="24dp" />
|
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="16dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="16dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/server_address"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textColor="?android:textColorSecondary" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/serverAddressEditText"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/server_address_hint"
|
|
||||||
android:inputType="textNoSuggestions"
|
|
||||||
android:padding="12dp"
|
|
||||||
android:textSize="16sp" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/server_port"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textColor="?android:textColorSecondary"
|
|
||||||
android:layout_marginTop="16dp" />
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/serverPortEditText"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/server_port_hint"
|
|
||||||
android:inputType="number"
|
|
||||||
android:padding="12dp"
|
|
||||||
android:textSize="16sp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:gravity="center_vertical">
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/saveButton"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:text="@string/save" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
|
||||||
android:id="@+id/testButton"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_marginStart="8dp"
|
|
||||||
android:text="@string/connection_test"
|
|
||||||
style="@style/Widget.Material3.Button.OutlinedButton" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/testResultText"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="12dp"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</ScrollView>
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="4dp"
|
|
||||||
app:cardCornerRadius="12dp"
|
|
||||||
app:cardElevation="2dp"
|
|
||||||
app:strokeWidth="0dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="12dp"
|
|
||||||
android:gravity="center_horizontal">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/appIcon"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:scaleType="centerInside"
|
|
||||||
android:contentDescription="App icon" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/appName"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:textSize="13sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:maxLines="2"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textColor="?android:textColorPrimary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/appVersion"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="2dp"
|
|
||||||
android:textSize="11sp"
|
|
||||||
android:textColor="?android:textColorSecondary" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
@ -1,64 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="4dp"
|
|
||||||
app:cardCornerRadius="8dp"
|
|
||||||
app:cardElevation="1dp"
|
|
||||||
app:strokeWidth="0dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:padding="12dp"
|
|
||||||
android:gravity="center_vertical">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/appIcon"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:scaleType="centerInside"
|
|
||||||
android:contentDescription="App icon" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_marginStart="12dp"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/appName"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="15sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="?android:textColorPrimary" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/appVersion"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="2dp"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:textColor="?android:textColorSecondary" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/updateBadge"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/badge_background"
|
|
||||||
android:paddingHorizontal="8dp"
|
|
||||||
android:paddingVertical="4dp"
|
|
||||||
android:text="@string/updating"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:textSize="11sp"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item
|
|
||||||
android:id="@+id/nav_home"
|
|
||||||
android:icon="@android:drawable/ic_menu_gallery"
|
|
||||||
android:title="@string/nav_home" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/nav_my_apps"
|
|
||||||
android:icon="@android:drawable/ic_menu_myplaces"
|
|
||||||
android:title="@string/nav_my_apps" />
|
|
||||||
<item
|
|
||||||
android:id="@+id/nav_settings"
|
|
||||||
android:icon="@android:drawable/ic_menu_preferences"
|
|
||||||
android:title="@string/nav_settings" />
|
|
||||||
</menu>
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/primary" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/primary" />
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
|
||||||
</adaptive-icon>
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:id="@+id/nav_graph"
|
|
||||||
app:startDestination="@id/nav_home">
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/nav_home"
|
|
||||||
android:name="com.localstore.ui.home.HomeFragment"
|
|
||||||
android:label="@string/nav_home" />
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/nav_my_apps"
|
|
||||||
android:name="com.localstore.ui.myapps.MyAppsFragment"
|
|
||||||
android:label="@string/nav_my_apps" />
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/nav_settings"
|
|
||||||
android:name="com.localstore.ui.settings.SettingsFragment"
|
|
||||||
android:label="@string/nav_settings" />
|
|
||||||
|
|
||||||
<fragment
|
|
||||||
android:id="@+id/nav_details"
|
|
||||||
android:name="com.localstore.ui.details.DetailsFragment"
|
|
||||||
android:label="App Details">
|
|
||||||
<argument
|
|
||||||
android:name="app_id"
|
|
||||||
app:argType="string" />
|
|
||||||
</fragment>
|
|
||||||
|
|
||||||
</navigation>
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="primary">#1B96FF</color>
|
|
||||||
<color name="primary_dark">#006EC4</color>
|
|
||||||
<color name="primary_light">#73BFFF</color>
|
|
||||||
<color name="accent">#FF4081</color>
|
|
||||||
<color name="background">#FAFAFA</color>
|
|
||||||
<color name="surface">#FFFFFF</color>
|
|
||||||
<color name="on_primary">#FFFFFF</color>
|
|
||||||
<color name="on_background">#1C1C1C</color>
|
|
||||||
<color name="on_surface">#1C1C1C</color>
|
|
||||||
<color name="error">#B00020</color>
|
|
||||||
</resources>
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<string name="app_name">Local App Store</string>
|
|
||||||
<string name="default_server_host">192.168.8.128</string>
|
|
||||||
|
|
||||||
<!-- Navigation -->
|
|
||||||
<string name="nav_home">Apps</string>
|
|
||||||
<string name="nav_my_apps">My Apps</string>
|
|
||||||
<string name="nav_settings">Settings</string>
|
|
||||||
|
|
||||||
<!-- Home Screen -->
|
|
||||||
<string name="search_apps">Search apps...</string>
|
|
||||||
<string name="no_apps_found">No apps found</string>
|
|
||||||
<string name="pull_to_refresh">Pull to refresh</string>
|
|
||||||
|
|
||||||
<!-- App Details -->
|
|
||||||
<string name="install">Install</string>
|
|
||||||
<string name="updating">Update</string>
|
|
||||||
<string name="installed">Installed</string>
|
|
||||||
<string name="downloading">Downloading...</string>
|
|
||||||
<string name="installing">Installing...</string>
|
|
||||||
<string name="description">Description</string>
|
|
||||||
<string name="version">Version</string>
|
|
||||||
<string name="size">Size</string>
|
|
||||||
<string name="package_name">Package</string>
|
|
||||||
<string name="permissions">Permissions</string>
|
|
||||||
|
|
||||||
<!-- My Apps -->
|
|
||||||
<string name="no_installed_apps">No apps installed from this store</string>
|
|
||||||
<string name="check_for_updates">Check for updates</string>
|
|
||||||
<string name="updates_available">Updates available</string>
|
|
||||||
<string name="up_to_date">All apps up to date</string>
|
|
||||||
|
|
||||||
<!-- Settings -->
|
|
||||||
<string name="server_settings">Server Settings</string>
|
|
||||||
<string name="server_address">Server Address</string>
|
|
||||||
<string name="server_address_hint">e.g. 192.168.8.128</string>
|
|
||||||
<string name="server_port">Server Port</string>
|
|
||||||
<string name="server_port_hint">e.g. 9800</string>
|
|
||||||
<string name="connection_test">Test Connection</string>
|
|
||||||
<string name="connection_success">Connected! %d apps available.</string>
|
|
||||||
<string name="connection_failed">Connection failed. Check server address.</string>
|
|
||||||
<string name="save">Save</string>
|
|
||||||
|
|
||||||
<!-- Install Results -->
|
|
||||||
<string name="install_success">App installed successfully</string>
|
|
||||||
<string name="install_failed">Installation failed: %s</string>
|
|
||||||
<string name="download_failed">Download failed: %s</string>
|
|
||||||
|
|
||||||
<!-- Errors -->
|
|
||||||
<string name="error_network">Network error. Check your connection.</string>
|
|
||||||
<string name="error_unknown">An unexpected error occurred.</string>
|
|
||||||
<string name="grant_install_permission">Please grant "Install unknown apps" permission in Settings.</string>
|
|
||||||
</resources>
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
<resources>
|
|
||||||
<style name="Theme.LocalAppStore" parent="Theme.Material3.Light.NoActionBar">
|
|
||||||
<item name="colorPrimary">@color/primary</item>
|
|
||||||
<item name="colorPrimaryDark">@color/primary_dark</item>
|
|
||||||
<item name="colorAccent">@color/accent</item>
|
|
||||||
<item name="android:colorBackground">@color/background</item>
|
|
||||||
<item name="colorOnPrimary">@color/on_primary</item>
|
|
||||||
<item name="colorOnBackground">@color/on_background</item>
|
|
||||||
<item name="colorOnSurface">@color/on_surface</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<network-security-config>
|
|
||||||
<domain-config cleartextTrafficPermitted="true">
|
|
||||||
<domain includeSubdomains="true">192.168.8.128</domain>
|
|
||||||
<domain includeSubdomains="true">192.168.</domain>
|
|
||||||
<domain includeSubdomains="true">10.0.</domain>
|
|
||||||
<domain includeSubdomains="true">localhost</domain>
|
|
||||||
</domain-config>
|
|
||||||
</network-security-config>
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'com.android.application' version '8.5.2' apply false
|
|
||||||
id 'org.jetbrains.kotlin.android' version '1.9.23' apply false
|
|
||||||
id 'com.google.devtools.ksp' version '1.9.23-1.0.20' apply false
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.register("clean", Delete) {
|
|
||||||
delete rootProject.buildDir
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
||||||
android.useAndroidX=true
|
|
||||||
kotlin.code.style=official
|
|
||||||
android.nonTransitiveRClass=true
|
|
||||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
@ -1,7 +0,0 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
249
android/gradlew
vendored
249
android/gradlew
vendored
@ -1,249 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright © 2015-2021 the original authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
|
||||||
#
|
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
app_path=$0
|
|
||||||
|
|
||||||
# Need this for daisy-chained symlinks.
|
|
||||||
while
|
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
|
||||||
[ -h "$app_path" ]
|
|
||||||
do
|
|
||||||
ls=$( ls -ld "$app_path" )
|
|
||||||
link=${ls#*' -> '}
|
|
||||||
case $link in #(
|
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# This is normally unused
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
APP_BASE_NAME=${0##*/}
|
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD=maximum
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "$( uname )" in #(
|
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
|
||||||
Darwin* ) darwin=true ;; #(
|
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
|
||||||
NONSTOP* ) nonstop=true ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
|
||||||
else
|
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD=java
|
|
||||||
if ! command -v java >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|
||||||
case $MAX_FD in #(
|
|
||||||
max*)
|
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
|
||||||
warn "Could not query maximum file descriptor limit"
|
|
||||||
esac
|
|
||||||
case $MAX_FD in #(
|
|
||||||
'' | soft) :;; #(
|
|
||||||
*)
|
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
ulimit -n "$MAX_FD" ||
|
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
|
||||||
# * args from the command line
|
|
||||||
# * the main class name
|
|
||||||
# * -classpath
|
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if "$cygwin" || "$msys" ; then
|
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
for arg do
|
|
||||||
if
|
|
||||||
case $arg in #(
|
|
||||||
-*) false ;; # don't mess with options #(
|
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
|
||||||
# possibly modified.
|
|
||||||
#
|
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
|
||||||
# changing the positional parameters here affects neither the number of
|
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
|
||||||
# and any embedded shellness will be escaped.
|
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
|
||||||
# treated as '${Hostname}' itself on the command line.
|
|
||||||
|
|
||||||
set -- \
|
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
|
||||||
if ! command -v xargs >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "xargs is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
||||||
92
android/gradlew.bat
vendored
92
android/gradlew.bat
vendored
@ -1,92 +0,0 @@
|
|||||||
@rem
|
|
||||||
@rem Copyright 2015 the original author or authors.
|
|
||||||
@rem
|
|
||||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
@rem you may not use this file except in compliance with the License.
|
|
||||||
@rem You may obtain a copy of the License at
|
|
||||||
@rem
|
|
||||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
@rem
|
|
||||||
@rem Unless required by applicable law or agreed to in writing, software
|
|
||||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
@rem See the License for the specific language governing permissions and
|
|
||||||
@rem limitations under the License.
|
|
||||||
@rem
|
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
|
||||||
@rem ##########################################################################
|
|
||||||
@rem
|
|
||||||
@rem Gradle startup script for Windows
|
|
||||||
@rem
|
|
||||||
@rem ##########################################################################
|
|
||||||
|
|
||||||
@rem Set local scope for the variables with windows NT shell
|
|
||||||
if "%OS%"=="Windows_NT" setlocal
|
|
||||||
|
|
||||||
set DIRNAME=%~dp0
|
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
|
||||||
@rem This is normally unused
|
|
||||||
set APP_BASE_NAME=%~n0
|
|
||||||
set APP_HOME=%DIRNAME%
|
|
||||||
|
|
||||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
|
||||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
||||||
|
|
||||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
|
||||||
|
|
||||||
@rem Find java.exe
|
|
||||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
|
||||||
|
|
||||||
set JAVA_EXE=java.exe
|
|
||||||
%JAVA_EXE% -version >NUL 2>&1
|
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
|
||||||
|
|
||||||
echo. 1>&2
|
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
|
||||||
echo. 1>&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
|
||||||
echo location of your Java installation. 1>&2
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:findJavaFromJavaHome
|
|
||||||
set JAVA_HOME=%JAVA_HOME:"=%
|
|
||||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|
||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
|
||||||
|
|
||||||
echo. 1>&2
|
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
|
||||||
echo. 1>&2
|
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
|
||||||
echo location of your Java installation. 1>&2
|
|
||||||
|
|
||||||
goto fail
|
|
||||||
|
|
||||||
:execute
|
|
||||||
@rem Setup the command line
|
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
|
||||||
|
|
||||||
:end
|
|
||||||
@rem End local scope for the variables with windows NT shell
|
|
||||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
|
||||||
|
|
||||||
:fail
|
|
||||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
|
||||||
rem the _cmd.exe /c_ return code!
|
|
||||||
set EXIT_CODE=%ERRORLEVEL%
|
|
||||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
|
||||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
|
||||||
exit /b %EXIT_CODE%
|
|
||||||
|
|
||||||
:mainEnd
|
|
||||||
if "%OS%"=="Windows_NT" endlocal
|
|
||||||
|
|
||||||
:omega
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
pluginManagement {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
gradlePluginPortal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
|
||||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.name = "LocalAppStore"
|
|
||||||
include ':app'
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'com.android.application'
|
|
||||||
id 'org.jetbrains.kotlin.android'
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace 'com.helloworld'
|
|
||||||
compileSdk 34
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
applicationId "com.helloworld"
|
|
||||||
minSdk 26
|
|
||||||
targetSdk 34
|
|
||||||
versionCode 1
|
|
||||||
versionName "1.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
signingConfigs {
|
|
||||||
release {
|
|
||||||
storeFile file("${System.getenv('HOME')}/playground/app-store/signing/localstore.keystore")
|
|
||||||
storePassword "localstore123"
|
|
||||||
keyAlias "localstore"
|
|
||||||
keyPassword "localstore123"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buildTypes {
|
|
||||||
release {
|
|
||||||
minifyEnabled false
|
|
||||||
signingConfig signingConfigs.release
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility JavaVersion.VERSION_17
|
|
||||||
targetCompatibility JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
kotlinOptions {
|
|
||||||
jvmTarget = "17"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation 'androidx.core:core-ktx:1.13.1'
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
|
||||||
implementation 'com.google.android.material:material:1.12.0'
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<application
|
|
||||||
android:allowBackup="true"
|
|
||||||
android:icon="@drawable/ic_launcher_foreground"
|
|
||||||
android:label="@string/app_name"
|
|
||||||
android:supportsRtl="true"
|
|
||||||
android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".MainActivity"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
package com.helloworld
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setContentView(R.layout.activity_main)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="108dp"
|
|
||||||
android:height="108dp"
|
|
||||||
android:viewportWidth="108"
|
|
||||||
android:viewportHeight="108">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M54,30 C65,30 74,39 74,50 C74,61 65,70 54,70 C43,70 34,61 34,50 C34,39 43,30 54,30Z"/>
|
|
||||||
<path
|
|
||||||
android:fillColor="#1B96FF"
|
|
||||||
android:pathData="M54,40 C59,40 63,44 63,49 C63,54 59,58 54,58 C49,58 45,54 45,49 C45,44 49,40 54,40Z"/>
|
|
||||||
</vector>
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:padding="32dp">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/hello_world"
|
|
||||||
android:textSize="28sp"
|
|
||||||
android:textColor="#1B96FF"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:gravity="center" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="from Local App Store"
|
|
||||||
android:textSize="16sp"
|
|
||||||
android:textColor="#666666"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:gravity="center" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
||||||
</adaptive-icon>
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#1B96FF</color>
|
|
||||||
</resources>
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<string name="app_name">Hello World</string>
|
|
||||||
<string name="hello_world">Hello World!</string>
|
|
||||||
</resources>
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id 'com.android.application' version '8.5.2' apply false
|
|
||||||
id 'org.jetbrains.kotlin.android' version '1.9.23' apply false
|
|
||||||
}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
android.useAndroidX=true
|
|
||||||
BIN
helloworld/gradle/wrapper/gradle-wrapper.jar
vendored
BIN
helloworld/gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
@ -1,7 +0,0 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
249
helloworld/gradlew
vendored
249
helloworld/gradlew
vendored
@ -1,249 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
#
|
|
||||||
# Copyright © 2015-2021 the original authors.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# https://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
#
|
|
||||||
|
|
||||||
##############################################################################
|
|
||||||
#
|
|
||||||
# Gradle start up script for POSIX generated by Gradle.
|
|
||||||
#
|
|
||||||
# Important for running:
|
|
||||||
#
|
|
||||||
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
|
||||||
# noncompliant, but you have some other compliant shell such as ksh or
|
|
||||||
# bash, then to run this script, type that shell name before the whole
|
|
||||||
# command line, like:
|
|
||||||
#
|
|
||||||
# ksh Gradle
|
|
||||||
#
|
|
||||||
# Busybox and similar reduced shells will NOT work, because this script
|
|
||||||
# requires all of these POSIX shell features:
|
|
||||||
# * functions;
|
|
||||||
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
|
||||||
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
|
||||||
# * compound commands having a testable exit status, especially «case»;
|
|
||||||
# * various built-in commands including «command», «set», and «ulimit».
|
|
||||||
#
|
|
||||||
# Important for patching:
|
|
||||||
#
|
|
||||||
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
|
||||||
# by Bash, Ksh, etc; in particular arrays are avoided.
|
|
||||||
#
|
|
||||||
# The "traditional" practice of packing multiple parameters into a
|
|
||||||
# space-separated string is a well documented source of bugs and security
|
|
||||||
# problems, so this is (mostly) avoided, by progressively accumulating
|
|
||||||
# options in "$@", and eventually passing that to Java.
|
|
||||||
#
|
|
||||||
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
|
||||||
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
|
||||||
# see the in-line comments for details.
|
|
||||||
#
|
|
||||||
# There are tweaks for specific operating systems such as AIX, CygWin,
|
|
||||||
# Darwin, MinGW, and NonStop.
|
|
||||||
#
|
|
||||||
# (3) This script is generated from the Groovy template
|
|
||||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
|
||||||
# within the Gradle project.
|
|
||||||
#
|
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
|
||||||
#
|
|
||||||
##############################################################################
|
|
||||||
|
|
||||||
# Attempt to set APP_HOME
|
|
||||||
|
|
||||||
# Resolve links: $0 may be a link
|
|
||||||
app_path=$0
|
|
||||||
|
|
||||||
# Need this for daisy-chained symlinks.
|
|
||||||
while
|
|
||||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
|
||||||
[ -h "$app_path" ]
|
|
||||||
do
|
|
||||||
ls=$( ls -ld "$app_path" )
|
|
||||||
link=${ls#*' -> '}
|
|
||||||
case $link in #(
|
|
||||||
/*) app_path=$link ;; #(
|
|
||||||
*) app_path=$APP_HOME$link ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
# This is normally unused
|
|
||||||
# shellcheck disable=SC2034
|
|
||||||
APP_BASE_NAME=${0##*/}
|
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
|
||||||
MAX_FD=maximum
|
|
||||||
|
|
||||||
warn () {
|
|
||||||
echo "$*"
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
die () {
|
|
||||||
echo
|
|
||||||
echo "$*"
|
|
||||||
echo
|
|
||||||
exit 1
|
|
||||||
} >&2
|
|
||||||
|
|
||||||
# OS specific support (must be 'true' or 'false').
|
|
||||||
cygwin=false
|
|
||||||
msys=false
|
|
||||||
darwin=false
|
|
||||||
nonstop=false
|
|
||||||
case "$( uname )" in #(
|
|
||||||
CYGWIN* ) cygwin=true ;; #(
|
|
||||||
Darwin* ) darwin=true ;; #(
|
|
||||||
MSYS* | MINGW* ) msys=true ;; #(
|
|
||||||
NONSTOP* ) nonstop=true ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
|
||||||
if [ -n "$JAVA_HOME" ] ; then
|
|
||||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
|
||||||
# IBM's JDK on AIX uses strange locations for the executables
|
|
||||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
|
||||||
else
|
|
||||||
JAVACMD=$JAVA_HOME/bin/java
|
|
||||||
fi
|
|
||||||
if [ ! -x "$JAVACMD" ] ; then
|
|
||||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
JAVACMD=java
|
|
||||||
if ! command -v java >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
|
||||||
location of your Java installation."
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|
||||||
case $MAX_FD in #(
|
|
||||||
max*)
|
|
||||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
|
||||||
warn "Could not query maximum file descriptor limit"
|
|
||||||
esac
|
|
||||||
case $MAX_FD in #(
|
|
||||||
'' | soft) :;; #(
|
|
||||||
*)
|
|
||||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
|
||||||
# shellcheck disable=SC2039,SC3045
|
|
||||||
ulimit -n "$MAX_FD" ||
|
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Collect all arguments for the java command, stacking in reverse order:
|
|
||||||
# * args from the command line
|
|
||||||
# * the main class name
|
|
||||||
# * -classpath
|
|
||||||
# * -D...appname settings
|
|
||||||
# * --module-path (only if needed)
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
|
||||||
|
|
||||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
|
||||||
if "$cygwin" || "$msys" ; then
|
|
||||||
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
|
||||||
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
|
||||||
|
|
||||||
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
|
||||||
|
|
||||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
|
||||||
for arg do
|
|
||||||
if
|
|
||||||
case $arg in #(
|
|
||||||
-*) false ;; # don't mess with options #(
|
|
||||||
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
|
||||||
[ -e "$t" ] ;; #(
|
|
||||||
*) false ;;
|
|
||||||
esac
|
|
||||||
then
|
|
||||||
arg=$( cygpath --path --ignore --mixed "$arg" )
|
|
||||||
fi
|
|
||||||
# Roll the args list around exactly as many times as the number of
|
|
||||||
# args, so each arg winds up back in the position where it started, but
|
|
||||||
# possibly modified.
|
|
||||||
#
|
|
||||||
# NB: a `for` loop captures its iteration list before it begins, so
|
|
||||||
# changing the positional parameters here affects neither the number of
|
|
||||||
# iterations, nor the values presented in `arg`.
|
|
||||||
shift # remove old arg
|
|
||||||
set -- "$@" "$arg" # push replacement arg
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
|
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
|
||||||
# and any embedded shellness will be escaped.
|
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
|
||||||
# treated as '${Hostname}' itself on the command line.
|
|
||||||
|
|
||||||
set -- \
|
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
|
||||||
-classpath "$CLASSPATH" \
|
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
|
||||||
"$@"
|
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
|
||||||
if ! command -v xargs >/dev/null 2>&1
|
|
||||||
then
|
|
||||||
die "xargs is not available"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use "xargs" to parse quoted args.
|
|
||||||
#
|
|
||||||
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
|
||||||
#
|
|
||||||
# In Bash we could simply go:
|
|
||||||
#
|
|
||||||
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
|
||||||
# set -- "${ARGS[@]}" "$@"
|
|
||||||
#
|
|
||||||
# but POSIX shell has neither arrays nor command substitution, so instead we
|
|
||||||
# post-process each arg (as a line of input to sed) to backslash-escape any
|
|
||||||
# character that might be a shell metacharacter, then use eval to reverse
|
|
||||||
# that process (while maintaining the separation between arguments), and wrap
|
|
||||||
# the whole thing up as a single "set" statement.
|
|
||||||
#
|
|
||||||
# This will of course break if any of these variables contains a newline or
|
|
||||||
# an unmatched quote.
|
|
||||||
#
|
|
||||||
|
|
||||||
eval "set -- $(
|
|
||||||
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
|
||||||
xargs -n1 |
|
|
||||||
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
|
||||||
tr '\n' ' '
|
|
||||||
)" '"$@"'
|
|
||||||
|
|
||||||
exec "$JAVACMD" "$@"
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
pluginManagement {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
gradlePluginPortal()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencyResolutionManagement {
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rootProject.name = "HelloWorld"
|
|
||||||
include ':app'
|
|
||||||
Loading…
x
Reference in New Issue
Block a user