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