Skip to content

Commit 6fbc3b7

Browse files
committed
Allow install app from web
1 parent 0c4a5a6 commit 6fbc3b7

File tree

4 files changed

+101
-22
lines changed

4 files changed

+101
-22
lines changed

app/src/main/java/com/ismartcoding/plain/features/PackageHelper.kt

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,22 @@ object PackageHelper {
2929
private val appTypeCache: MutableMap<String, String> = HashMap()
3030
private val appCertsCache: MutableMap<String, List<DCertificate>> = HashMap()
3131

32-
fun getPackageStatuses(ids: List<String>): Map<String, Boolean> {
33-
val packages = packageManager.getInstalledPackages(0).map { it.packageName }
34-
val map = mutableMapOf<String, Boolean>()
32+
fun getPackageInfoMap(ids: List<String>): Map<String, PackageInfo?> {
33+
val packages = packageManager.getInstalledPackages(0).associateBy { it.packageName }
34+
val map = mutableMapOf<String, PackageInfo?>()
3535
ids.forEach { id ->
36-
map[id] = packages.contains(id)
36+
map[id] = packages[id]
3737
}
3838

3939
return map
4040
}
4141

42+
fun isInstalled(packageName: String): Boolean {
43+
return getPackageInfoMap(listOf(packageName))[packageName] != null
44+
}
45+
4246
fun isUninstalled(packageName: String): Boolean {
43-
return getPackageStatuses(listOf(packageName))[packageName] == false
47+
return !isInstalled(packageName)
4448
}
4549

4650
suspend fun searchAsync(query: String, limit: Int, offset: Int, sortBy: FileSortBy): List<DPackage> {
@@ -215,11 +219,14 @@ object PackageHelper {
215219
if (query.isEmpty()) {
216220
return packageManager.getInstalledApplications(0).count()
217221
} else {
218-
val t = QueryHelper.parseAsync(query).find { it.name == "type" }
219-
if (t != null) {
220-
val type = t.value
221-
return packageManager.getInstalledApplications(0).count { appInfo ->
222-
getAppType(appInfo) == type
222+
val parsed = QueryHelper.parseAsync(query)
223+
if (parsed.size == 1) {
224+
val t = parsed.find { it.name == "type" }
225+
if (t != null) {
226+
val type = t.value
227+
return packageManager.getInstalledApplications(0).count { appInfo ->
228+
getAppType(appInfo) == type
229+
}
223230
}
224231
}
225232
}
@@ -307,6 +314,36 @@ object PackageHelper {
307314
})
308315
}
309316

317+
fun install(context: Context, file: File) {
318+
try {
319+
if (!file.name.lowercase().endsWith(".apk")) {
320+
LogCat.e("Invalid file extension: ${file.name}")
321+
throw IllegalArgumentException("Invalid file extension: ${file.name}")
322+
}
323+
324+
val uri = androidx.core.content.FileProvider.getUriForFile(
325+
context,
326+
com.ismartcoding.plain.Constants.AUTHORITY,
327+
file
328+
)
329+
330+
val intent = Intent(Intent.ACTION_VIEW).apply {
331+
setDataAndType(uri, "application/vnd.android.package-archive")
332+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
333+
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
334+
putExtra(Intent.EXTRA_RETURN_RESULT, true)
335+
putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
336+
putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, context.applicationInfo.packageName)
337+
}
338+
339+
context.startActivity(intent)
340+
LogCat.d("APK installation intent started for ${file.name}")
341+
} catch (e: Exception) {
342+
LogCat.e("Failed to install APK: ${e.message}", e)
343+
throw e
344+
}
345+
}
346+
310347
private fun List<DPackage>.sorted(sortBy: FileSortBy): List<DPackage> {
311348
return when (sortBy) {
312349
FileSortBy.NAME_ASC -> this.sortedBy { Pinyin.toPinyin(it.name).lowercase() }

app/src/main/java/com/ismartcoding/plain/web/SXGraphQL.kt

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import com.apurebase.kgraphql.schema.dsl.SchemaBuilder
1212
import com.apurebase.kgraphql.schema.dsl.SchemaConfigurationDSL
1313
import com.apurebase.kgraphql.schema.execution.Execution
1414
import com.apurebase.kgraphql.schema.execution.Executor
15+
import com.ismartcoding.lib.apk.ApkParsers
1516
import com.ismartcoding.lib.channel.sendEvent
1617
import com.ismartcoding.lib.extensions.cut
1718
import com.ismartcoding.lib.extensions.getFinalPath
@@ -44,20 +45,22 @@ import com.ismartcoding.plain.db.DMessageType
4445
import com.ismartcoding.plain.enums.AppFeatureType
4546
import com.ismartcoding.plain.enums.DataType
4647
import com.ismartcoding.plain.enums.MediaPlayMode
47-
import com.ismartcoding.plain.extensions.newPath
48-
import com.ismartcoding.plain.extensions.sorted
49-
import com.ismartcoding.plain.features.AudioPlayer
5048
import com.ismartcoding.plain.events.CancelNotificationsEvent
51-
import com.ismartcoding.plain.features.ChatHelper
5249
import com.ismartcoding.plain.events.ClearAudioPlaylistEvent
5350
import com.ismartcoding.plain.events.DeleteChatItemViewEvent
51+
import com.ismartcoding.plain.events.EventType
52+
import com.ismartcoding.plain.events.FetchLinkPreviewsEvent
5453
import com.ismartcoding.plain.events.HttpApiEvents
55-
import com.ismartcoding.plain.features.LinkPreviewHelper
54+
import com.ismartcoding.plain.events.StartScreenMirrorEvent
55+
import com.ismartcoding.plain.events.WebSocketEvent
56+
import com.ismartcoding.plain.extensions.newPath
57+
import com.ismartcoding.plain.extensions.sorted
58+
import com.ismartcoding.plain.features.AudioPlayer
59+
import com.ismartcoding.plain.features.ChatHelper
5660
import com.ismartcoding.plain.features.NoteHelper
5761
import com.ismartcoding.plain.features.PackageHelper
5862
import com.ismartcoding.plain.features.Permission
5963
import com.ismartcoding.plain.features.Permissions
60-
import com.ismartcoding.plain.events.StartScreenMirrorEvent
6164
import com.ismartcoding.plain.features.TagHelper
6265
import com.ismartcoding.plain.features.call.SimHelper
6366
import com.ismartcoding.plain.features.contact.GroupHelper
@@ -79,6 +82,7 @@ import com.ismartcoding.plain.helpers.DeviceInfoHelper
7982
import com.ismartcoding.plain.helpers.FileHelper
8083
import com.ismartcoding.plain.helpers.QueryHelper
8184
import com.ismartcoding.plain.helpers.TempHelper
85+
import com.ismartcoding.plain.packageManager
8286
import com.ismartcoding.plain.preference.ApiPermissionsPreference
8387
import com.ismartcoding.plain.preference.AudioPlayModePreference
8488
import com.ismartcoding.plain.preference.AudioPlayingPreference
@@ -112,16 +116,14 @@ import com.ismartcoding.plain.web.models.MediaFileInfo
112116
import com.ismartcoding.plain.web.models.Message
113117
import com.ismartcoding.plain.web.models.Note
114118
import com.ismartcoding.plain.web.models.NoteInput
119+
import com.ismartcoding.plain.web.models.PackageInstallPending
115120
import com.ismartcoding.plain.web.models.PackageStatus
116121
import com.ismartcoding.plain.web.models.StorageStats
117122
import com.ismartcoding.plain.web.models.Tag
118123
import com.ismartcoding.plain.web.models.TempValue
119124
import com.ismartcoding.plain.web.models.Video
120125
import com.ismartcoding.plain.web.models.toExportModel
121126
import com.ismartcoding.plain.web.models.toModel
122-
import com.ismartcoding.plain.events.EventType
123-
import com.ismartcoding.plain.events.FetchLinkPreviewsEvent
124-
import com.ismartcoding.plain.events.WebSocketEvent
125127
import com.ismartcoding.plain.workers.FeedFetchWorker
126128
import io.ktor.http.ContentType
127129
import io.ktor.http.HttpStatusCode
@@ -149,7 +151,6 @@ import kotlinx.serialization.json.put
149151
import java.io.File
150152
import java.io.StringReader
151153
import java.io.StringWriter
152-
import kotlin.collections.set
153154
import kotlin.io.path.Path
154155
import kotlin.io.path.moveTo
155156

@@ -435,7 +436,11 @@ class SXGraphQL(val schema: Schema) {
435436
}
436437
query("packageStatuses") {
437438
resolver { ids: List<ID> ->
438-
PackageHelper.getPackageStatuses(ids.map { it.value }).map { PackageStatus(ID(it.key), it.value) }
439+
PackageHelper.getPackageInfoMap(ids.map { it.value }).map {
440+
val pkg = it.value
441+
val updatedAt = if (pkg != null) Instant.fromEpochMilliseconds(pkg.lastUpdateTime) else null
442+
PackageStatus(ID(it.key), pkg != null, updatedAt)
443+
}
439444
}
440445
}
441446
query("packageCount") {
@@ -682,6 +687,38 @@ class SXGraphQL(val schema: Schema) {
682687
true
683688
}
684689
}
690+
mutation("installPackage") {
691+
resolver { path: String ->
692+
val file = File(path)
693+
if (!file.exists()) {
694+
throw GraphQLError("File does not exist")
695+
}
696+
697+
try {
698+
val context = MainActivity.instance.get()!!
699+
if (file.name.endsWith(".apk", ignoreCase = true)) {
700+
LogCat.d("Installing APK file: ${file.name}")
701+
val apkMeta = ApkParsers.getMetaInfo(file)
702+
?: throw GraphQLError("Failed to parse APK package ID")
703+
704+
PackageHelper.install(context, file)
705+
val packageName = apkMeta.packageName ?: ""
706+
try {
707+
val pkg = packageManager.getPackageInfo(packageName, 0)
708+
PackageInstallPending(packageName, Instant.fromEpochMilliseconds(pkg.lastUpdateTime), isNew = false)
709+
} catch (e: Exception) {
710+
PackageInstallPending(packageName, null, isNew = true)
711+
}
712+
} else {
713+
throw GraphQLError("Unsupported file format. Only APK files are supported.")
714+
}
715+
} catch (e: Exception) {
716+
LogCat.e("Installation failed: ${e.message}", e)
717+
throw GraphQLError("Installation failed: ${e.message}")
718+
}
719+
}
720+
}
721+
685722
mutation("cancelNotifications") {
686723
resolver { ids: List<ID> ->
687724
sendEvent(CancelNotificationsEvent(ids.map { it.value }.toSet()))

app/src/main/java/com/ismartcoding/plain/web/models/Package.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,4 @@ fun DPackage.toModel(): Package {
2525
)
2626
}
2727

28-
data class PackageStatus(val id: ID, val exist: Boolean)
28+
data class PackageStatus(val id: ID, val exist: Boolean, val updatedAt: Instant?)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.ismartcoding.plain.web.models
2+
3+
import kotlinx.datetime.Instant
4+
5+
data class PackageInstallPending(val packageName: String, val updatedAt: Instant?, val isNew: Boolean)

0 commit comments

Comments
 (0)