diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8b1b2ec5359f2941460d1d1a7d55c64a1ea0f6f0..5adf95d9e43ea955db299341e0cc44516c76f8ea 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -100,6 +100,11 @@
android:foregroundServiceType="dataSync">
+
+
+
diff --git a/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt b/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt
index 96290fd68c36f7b33bee3a1b85e40c13a6d4e224..bdcbe1fedfcdec659758aa8e3e4d399e4a99cd3b 100644
--- a/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt
+++ b/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt
@@ -58,7 +58,7 @@ class ApplicationDataManager @Inject constructor(
application.package_name.isBlank() -> FilterLevel.UNKNOWN
!application.isFree && application.price.isBlank() -> FilterLevel.UI
application.source == Source.PWA || application.source == Source.OPEN_SOURCE -> FilterLevel.NONE
- application.source == Source.SYSTEM_APP -> FilterLevel.NONE
+ application.source == Source.SYSTEM_APP || application.source == Source.LOCAL_PWA -> FilterLevel.NONE
!isRestricted(application) -> FilterLevel.NONE
application.originalSize == 0L -> FilterLevel.UI
else -> FilterLevel.NONE
diff --git a/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt
index 8e6ca6d84b71ecfa13fa96d3bad6ebac7eff973d..94d111f83861c2555be810b4f78c1f41f4c73057 100644
--- a/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt
+++ b/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt
@@ -73,7 +73,7 @@ class DownloadInfoApiImpl @Inject constructor(
updateDownloadInfoFromGplay(appInstall, list)
}
- Source.SYSTEM_APP -> {
+ Source.SYSTEM_APP, Source.LOCAL_PWA -> {
return // nothing to do as downloadURLList is already set
}
}
diff --git a/app/src/main/java/foundation/e/apps/data/application/home/HomeApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/home/HomeApiImpl.kt
index e82c70db871f22b06e065f3c2d04bd5f060932fe..bca60285a43a9af3bbf68936f03b637db7a97e05 100644
--- a/app/src/main/java/foundation/e/apps/data/application/home/HomeApiImpl.kt
+++ b/app/src/main/java/foundation/e/apps/data/application/home/HomeApiImpl.kt
@@ -93,7 +93,7 @@ class HomeApiImpl @Inject constructor(
Source.PLAY_STORE -> ("GPlay home loading error\n" + apiStatus.message).trim()
Source.SYSTEM_APP -> ("Gitlab home not allowed\n" + apiStatus.message).trim()
Source.OPEN_SOURCE -> ("Open Source home loading error\n" + apiStatus.message).trim()
- Source.PWA -> ("PWA home loading error\n" + apiStatus.message).trim()
+ Source.PWA, Source.LOCAL_PWA -> ("PWA home loading error\n" + apiStatus.message).trim()
}
}
}
diff --git a/app/src/main/java/foundation/e/apps/data/enums/Source.kt b/app/src/main/java/foundation/e/apps/data/enums/Source.kt
index 6bafbc435030942645650388f5de72498313ceac..c5142b463af66ad93283a3b6b807de0e3863736a 100644
--- a/app/src/main/java/foundation/e/apps/data/enums/Source.kt
+++ b/app/src/main/java/foundation/e/apps/data/enums/Source.kt
@@ -19,6 +19,7 @@ package foundation.e.apps.data.enums
enum class Source {
OPEN_SOURCE,
+ LOCAL_PWA,
PWA,
SYSTEM_APP,
PLAY_STORE;
@@ -37,6 +38,7 @@ enum class Source {
return when (source) {
"Open Source" -> OPEN_SOURCE
"PWA" -> PWA
+ "Local PWA" -> LOCAL_PWA
"SYSTEM_APP" -> SYSTEM_APP
else -> PLAY_STORE
}
diff --git a/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt b/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt
index b1ea00b57efa1d0e304d4eb5db118b0dad21ad95..065da3d581e6247a18f9c5a470e12c06f015be87 100644
--- a/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt
+++ b/app/src/main/java/foundation/e/apps/install/pkg/PwaManager.kt
@@ -7,7 +7,9 @@ import android.content.Intent
import android.database.Cursor
import android.graphics.Bitmap
import android.graphics.BitmapFactory
+import android.graphics.drawable.BitmapDrawable
import android.net.Uri
+import androidx.core.content.ContextCompat
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
@@ -24,6 +26,7 @@ import java.io.IOException
import java.net.URL
import javax.inject.Inject
import javax.inject.Singleton
+import foundation.e.apps.data.enums.Source
@Singleton
@OpenForTesting
@@ -41,7 +44,7 @@ class PwaManager @Inject constructor(
private const val PWA_NAME = "PWA_NAME"
private const val PWA_ID = "PWA_ID"
- private const val PWA_PLAYER = "content://foundation.e.pwaplayer.provider/pwa"
+ const val PWA_PLAYER = "content://foundation.e.pwaplayer.provider/pwa"
private const val VIEW_PWA = "foundation.e.blisslauncher.VIEW_PWA"
private const val DELAY_100 = 100L
@@ -117,7 +120,12 @@ class PwaManager @Inject constructor(
appInstallRepository.updateDownload(appInstall)
// Get bitmap and byteArray for icon
- val iconBitmap = getIconImageBitmap(appInstall.getAppIconUrl())
+ val iconBitmap = if (appInstall.source != Source.LOCAL_PWA) {
+ getIconImageBitmap(appInstall.getAppIconUrl())
+ } else {
+ val resourceId = appInstall.getAppIconUrl().toInt()
+ getIconImageBitmapFromDrawable(context, resourceId)
+ }
if (iconBitmap == null) {
appInstall.status = Status.INSTALLATION_ISSUE
@@ -150,6 +158,15 @@ class PwaManager @Inject constructor(
}
}
+ fun getIconImageBitmapFromDrawable(context: Context, resourceId: Int): Bitmap? {
+ val drawable = ContextCompat.getDrawable(context, resourceId)
+ if (drawable is BitmapDrawable) {
+ return drawable.bitmap
+ }
+
+ return null
+ }
+
fun Bitmap.toByteArray(): ByteArray {
val byteArrayOS = ByteArrayOutputStream()
this.compress(Bitmap.CompressFormat.PNG, BITMAP_QUALITY, byteArrayOS)
diff --git a/app/src/main/java/foundation/e/apps/install/updates/UpdatesBroadcastReceiver.kt b/app/src/main/java/foundation/e/apps/install/updates/UpdatesBroadcastReceiver.kt
index 36e89a2470e0a022e1e25620028d31cdcaf156aa..65c4bc7c49eb13eb63e38220b64b4a4f9e08e47c 100644
--- a/app/src/main/java/foundation/e/apps/install/updates/UpdatesBroadcastReceiver.kt
+++ b/app/src/main/java/foundation/e/apps/install/updates/UpdatesBroadcastReceiver.kt
@@ -20,8 +20,10 @@ package foundation.e.apps.install.updates
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
+import androidx.core.content.ContextCompat
import androidx.work.ExistingPeriodicWorkPolicy
import foundation.e.apps.data.preference.AppLoungePreference
+import foundation.e.apps.utils.LocalPWAInstaller
import timber.log.Timber
class UpdatesBroadcastReceiver : BroadcastReceiver() {
@@ -32,6 +34,9 @@ class UpdatesBroadcastReceiver : BroadcastReceiver() {
val appLoungePreference = AppLoungePreference(context)
val interval = appLoungePreference.getUpdateInterval()
UpdatesWorkManager.enqueueWork(context, interval, ExistingPeriodicWorkPolicy.REPLACE)
+
+ val serviceIntent = Intent(context, LocalPWAInstaller::class.java)
+ ContextCompat.startForegroundService(context, serviceIntent)
}
}
}
diff --git a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt
index 58cc4ca8196a592b58b406374a3504d1ec6e89fb..5aade84fb38b30cecaa084af08c420149b31d377 100644
--- a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt
+++ b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt
@@ -100,7 +100,8 @@ class AppInstallProcessor @Inject constructor(
it.contentRating = application.contentRating
}
- if (appInstall.type == Type.PWA || application.source == Source.SYSTEM_APP) {
+ if (appInstall.type == Type.PWA || application.source == Source.SYSTEM_APP
+ || appInstall.source == Source.LOCAL_PWA) {
appInstall.downloadURLList = mutableListOf(application.url)
}
diff --git a/app/src/main/java/foundation/e/apps/ui/application/model/ApplicationScreenshotsRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/application/model/ApplicationScreenshotsRVAdapter.kt
index 6119dfb12a89d9748a9b8087b3fdedf38199730b..ff2d791904b9a5b1d3e150e4bfbda27e08435045 100644
--- a/app/src/main/java/foundation/e/apps/ui/application/model/ApplicationScreenshotsRVAdapter.kt
+++ b/app/src/main/java/foundation/e/apps/ui/application/model/ApplicationScreenshotsRVAdapter.kt
@@ -61,7 +61,7 @@ class ApplicationScreenshotsRVAdapter(
Source.PLAY_STORE -> {
imageView.load(oldList[position])
}
- Source.SYSTEM_APP -> {
+ Source.SYSTEM_APP, Source.LOCAL_PWA -> {
// no operation
}
}
diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt
index b782362a4e4d344d96891e29424a3282c4cbb5d4..b17ac96af18afe42feafbbdccb7f40b920f63c7a 100644
--- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt
+++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt
@@ -188,6 +188,9 @@ class ApplicationListRVAdapter(
placeholder(shimmerDrawable)
}
}
+ Source.LOCAL_PWA -> {
+ // Do nothing
+ }
}
}
diff --git a/app/src/main/java/foundation/e/apps/utils/LocalPWAInstaller.kt b/app/src/main/java/foundation/e/apps/utils/LocalPWAInstaller.kt
new file mode 100644
index 0000000000000000000000000000000000000000..4824592624915f19abcb0d1804eb1e6f69347006
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/utils/LocalPWAInstaller.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2025 e Foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package foundation.e.apps.utils
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.IBinder
+import androidx.core.app.NotificationCompat
+import androidx.core.net.toUri
+import dagger.hilt.android.AndroidEntryPoint
+import foundation.e.apps.R
+import foundation.e.apps.data.enums.Source
+import foundation.e.apps.data.enums.Status
+import foundation.e.apps.data.install.models.AppInstall
+import foundation.e.apps.install.pkg.PwaManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import java.util.UUID
+import javax.inject.Inject
+import androidx.core.content.edit
+import foundation.e.apps.install.pkg.PwaManager.Companion.PWA_PLAYER
+
+
+@AndroidEntryPoint
+class LocalPWAInstaller : Service() {
+ @Inject
+ lateinit var pwaManager: PwaManager
+
+ override fun onCreate() {
+ super.onCreate()
+ getSystemService(NotificationManager::class.java).createNotificationChannel(
+ NotificationChannel(CHANNEL_ID, getString(R.string.pwa),
+ NotificationManager.IMPORTANCE_LOW)
+ )
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ // Ensure the service is in the foreground
+ // It is required to install PWA
+ startForeground(NOTIFICATION_ID, createNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
+
+ CoroutineScope(Dispatchers.IO).launch {
+ // Add a short delay to allow foreground state to settle
+ delay(DELAY_BEFORE_INSTALL_MS)
+ installPWA()
+ }
+
+ return START_NOT_STICKY
+ }
+
+ private fun createNotification() = NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle(getString(R.string.pwa))
+ .setContentText(getString(R.string.installing))
+ .setSmallIcon(R.drawable.app_lounge_notification_icon)
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .build()
+
+ private suspend fun installPWA() {
+ val preferences = getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE)
+
+ // Pwa info
+ val pwaUrl = "https://accounts.murena.io/auth"
+ val pwaName = "Workspace"
+
+ val localPwa = AppInstall(
+ id = UUID.randomUUID().toString(),
+ source = Source.LOCAL_PWA,
+ name = pwaName,
+ downloadURLList = mutableListOf(pwaUrl),
+ iconImageUrl = (R.drawable.murena_pwa_logo).toString()
+ )
+
+ val wasInstalledBefore = preferences.getBoolean(pwaUrl, false)
+ if (getPwaStatus(localPwa) != Status.INSTALLED && !wasInstalledBefore) {
+ pwaManager.installPWAApp(localPwa)
+ Timber.d("Installed PWA: ${localPwa.name}")
+
+ // Mark as installed
+ preferences.edit {
+ putBoolean(pwaUrl, true)
+ }
+ } else {
+ Timber.d("PWA already installed: ${localPwa.name}")
+ }
+
+ stopSelf()
+ }
+
+ private fun getPwaStatus(app: AppInstall): Status {
+ val urlToCheck = app.downloadURLList.firstOrNull() ?: return Status.UNAVAILABLE
+
+ val isInstalled = contentResolver.query(
+ PWA_PLAYER.toUri(),
+ arrayOf("url"),
+ null, null, null
+ )?.use { cursor ->
+ generateSequence { if (cursor.moveToNext()) cursor else null }
+ .any { it.getString(it.getColumnIndexOrThrow("url")) == urlToCheck }
+ } ?: false
+
+ return if (isInstalled) Status.INSTALLED else Status.UNAVAILABLE
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ companion object {
+ private const val CHANNEL_ID = "PWA_INSTALLATION_CHANNEL"
+ private const val PREFERENCES_FILE_NAME = "local_pwa_list"
+ private const val NOTIFICATION_ID = 1
+ private const val DELAY_BEFORE_INSTALL_MS = 1000L
+ }
+}
diff --git a/app/src/main/res/drawable-nodpi/murena_pwa_logo.webp b/app/src/main/res/drawable-nodpi/murena_pwa_logo.webp
new file mode 100644
index 0000000000000000000000000000000000000000..a21d79ab51458677f81b61cf0e90854aa0c20b52
Binary files /dev/null and b/app/src/main/res/drawable-nodpi/murena_pwa_logo.webp differ