diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b9335574405d1a2574844ecc88007a3968168f25..5fef1623c24ed4d1c91ab03771aa171a17d32cb2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -68,6 +68,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt
index b7cdbfe51add14f091fff311399cd506dfadde3f..c70c36d4a26bc0506eac8b55ef59578bdaafe204 100644
--- a/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt
+++ b/app/src/main/java/foundation/e/apps/applicationlist/model/ApplicationListRVAdapter.kt
@@ -48,6 +48,7 @@ import foundation.e.apps.updates.UpdatesFragmentDirections
import foundation.e.apps.utils.enums.Origin
import foundation.e.apps.utils.enums.Status
import foundation.e.apps.utils.enums.User
+import foundation.e.apps.utils.modules.PWAManagerModule
import javax.inject.Singleton
@Singleton
@@ -57,6 +58,7 @@ class ApplicationListRVAdapter(
private val fdroidFetchViewModel: FdroidFetchViewModel,
private val currentDestinationId: Int,
private val pkgManagerModule: PkgManagerModule,
+ private val pwaManagerModule: PWAManagerModule,
private val user: User,
private val lifecycleOwner: LifecycleOwner,
private val paidAppHandler: ((FusedApp) -> Unit)? = null
@@ -341,7 +343,11 @@ class ApplicationListRVAdapter(
backgroundTintList = ContextCompat.getColorStateList(view.context, R.color.colorAccent)
strokeColor = ContextCompat.getColorStateList(view.context, R.color.colorAccent)
setOnClickListener {
- context.startActivity(pkgManagerModule.getLaunchIntent(searchApp.package_name))
+ if (searchApp.is_pwa) {
+ pwaManagerModule.launchPwa(searchApp)
+ } else {
+ context.startActivity(pkgManagerModule.getLaunchIntent(searchApp.package_name))
+ }
}
}
}
diff --git a/app/src/main/java/foundation/e/apps/home/HomeFragment.kt b/app/src/main/java/foundation/e/apps/home/HomeFragment.kt
index f54faf6d0aa178119ad2dbe925b6fb7d31fb20c0..af754a46e26a71d410f56026ae4b8f02276a507d 100644
--- a/app/src/main/java/foundation/e/apps/home/HomeFragment.kt
+++ b/app/src/main/java/foundation/e/apps/home/HomeFragment.kt
@@ -42,6 +42,7 @@ import foundation.e.apps.manager.download.data.DownloadProgress
import foundation.e.apps.manager.pkg.PkgManagerModule
import foundation.e.apps.utils.enums.Status
import foundation.e.apps.utils.enums.User
+import foundation.e.apps.utils.modules.PWAManagerModule
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -58,6 +59,9 @@ class HomeFragment : Fragment(R.layout.fragment_home), FusedAPIInterface {
@Inject
lateinit var pkgManagerModule: PkgManagerModule
+ @Inject
+ lateinit var pwaManagerModule: PWAManagerModule
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentHomeBinding.bind(view)
@@ -83,6 +87,7 @@ class HomeFragment : Fragment(R.layout.fragment_home), FusedAPIInterface {
val homeParentRVAdapter = HomeParentRVAdapter(
this,
pkgManagerModule,
+ pwaManagerModule,
User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name),
mainActivityViewModel, viewLifecycleOwner
) { fusedApp ->
diff --git a/app/src/main/java/foundation/e/apps/home/model/HomeChildRVAdapter.kt b/app/src/main/java/foundation/e/apps/home/model/HomeChildRVAdapter.kt
index 0d4b74f3260b43010ee10d212ae7c6f58fe98c05..42997ea638107aa2e9f49a5cb276da4ae20b0921 100644
--- a/app/src/main/java/foundation/e/apps/home/model/HomeChildRVAdapter.kt
+++ b/app/src/main/java/foundation/e/apps/home/model/HomeChildRVAdapter.kt
@@ -40,10 +40,12 @@ import foundation.e.apps.manager.pkg.PkgManagerModule
import foundation.e.apps.utils.enums.Origin
import foundation.e.apps.utils.enums.Status
import foundation.e.apps.utils.enums.User
+import foundation.e.apps.utils.modules.PWAManagerModule
class HomeChildRVAdapter(
private val fusedAPIInterface: FusedAPIInterface,
private val pkgManagerModule: PkgManagerModule,
+ private val pwaManagerModule: PWAManagerModule,
private val user: User,
private val paidAppHandler: ((FusedApp) -> Unit)? = null
) : ListAdapter(HomeChildFusedAppDiffUtil()) {
@@ -106,7 +108,11 @@ class HomeChildRVAdapter(
strokeColor =
ContextCompat.getColorStateList(view.context, R.color.colorAccent)
setOnClickListener {
- context.startActivity(pkgManagerModule.getLaunchIntent(homeApp.package_name))
+ if (homeApp.is_pwa) {
+ pwaManagerModule.launchPwa(homeApp)
+ } else {
+ context.startActivity(pkgManagerModule.getLaunchIntent(homeApp.package_name))
+ }
}
}
}
diff --git a/app/src/main/java/foundation/e/apps/home/model/HomeParentRVAdapter.kt b/app/src/main/java/foundation/e/apps/home/model/HomeParentRVAdapter.kt
index 5d32668340ae803712a921da458565bdbaa4c75a..2883c6510b8d4b531307c2f45f50d770cc01fbc3 100644
--- a/app/src/main/java/foundation/e/apps/home/model/HomeParentRVAdapter.kt
+++ b/app/src/main/java/foundation/e/apps/home/model/HomeParentRVAdapter.kt
@@ -32,10 +32,12 @@ import foundation.e.apps.databinding.HomeParentListItemBinding
import foundation.e.apps.manager.database.fusedDownload.FusedDownload
import foundation.e.apps.manager.pkg.PkgManagerModule
import foundation.e.apps.utils.enums.User
+import foundation.e.apps.utils.modules.PWAManagerModule
class HomeParentRVAdapter(
private val fusedAPIInterface: FusedAPIInterface,
private val pkgManagerModule: PkgManagerModule,
+ private val pwaManagerModule: PWAManagerModule,
private val user: User,
private val mainActivityViewModel: MainActivityViewModel,
private val lifecycleOwner: LifecycleOwner,
@@ -56,7 +58,7 @@ class HomeParentRVAdapter(
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val fusedHome = getItem(position)
val homeChildRVAdapter =
- HomeChildRVAdapter(fusedAPIInterface, pkgManagerModule, user, paidAppHandler)
+ HomeChildRVAdapter(fusedAPIInterface, pkgManagerModule, pwaManagerModule, user, paidAppHandler)
homeChildRVAdapter.setData(fusedHome.list)
holder.binding.titleTV.text = fusedHome.title
diff --git a/app/src/main/java/foundation/e/apps/receiver/PWAPlayerStatusReceiver.kt b/app/src/main/java/foundation/e/apps/receiver/PWAPlayerStatusReceiver.kt
new file mode 100644
index 0000000000000000000000000000000000000000..264213926f2891827a6f348136ee5b26a598daad
--- /dev/null
+++ b/app/src/main/java/foundation/e/apps/receiver/PWAPlayerStatusReceiver.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022 ECORP
+ *
+ * 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.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import dagger.hilt.android.AndroidEntryPoint
+import foundation.e.apps.manager.database.DatabaseRepository
+import foundation.e.apps.utils.enums.Status
+import kotlinx.coroutines.*
+import javax.inject.Inject
+
+/**
+ * Illustration of how to get PWA installation status from broadcast from PWA player.
+ * This class is not of much use here as after a PWA is installed, the FusedDownload instance
+ * is deleted from the database.
+ *
+ * The sent intent contains following extras:
+ * 1. SHORTCUT_ID - string shortcut id.
+ * 2. URL - string url of the pwa.
+ */
+@AndroidEntryPoint
+@DelicateCoroutinesApi
+class PWAPlayerStatusReceiver: BroadcastReceiver() {
+
+ companion object {
+ const val ACTION_PWA_ADDED = "foundation.e.pwaplayer.PWA_ADDED"
+ const val ACTION_PWA_REMOVED = "foundation.e.pwaplayer.PWA_REMOVED"
+ }
+
+ @Inject
+ lateinit var databaseRepository: DatabaseRepository
+
+ override fun onReceive(context: Context?, intent: Intent?) {
+ GlobalScope.launch {
+ try {
+ intent?.getStringExtra("SHORTCUT_ID")?.let { shortcutId ->
+ databaseRepository.getDownloadById(shortcutId)?.let { fusedDownload ->
+ when (intent.action) {
+ ACTION_PWA_ADDED -> {
+ fusedDownload.status = Status.INSTALLED
+ databaseRepository.updateDownload(fusedDownload)
+ }
+ ACTION_PWA_REMOVED -> {
+ databaseRepository.deleteDownload(fusedDownload)
+ }
+ }
+ }
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt
index 77d8e04714f302ecbdbfa0db3680fd32b752b6b2..fad5b25487aee7a5aed3ac7b8a60b70acc3f5fe8 100644
--- a/app/src/main/java/foundation/e/apps/search/SearchFragment.kt
+++ b/app/src/main/java/foundation/e/apps/search/SearchFragment.kt
@@ -52,6 +52,7 @@ import foundation.e.apps.databinding.FragmentSearchBinding
import foundation.e.apps.manager.pkg.PkgManagerModule
import foundation.e.apps.utils.enums.Status
import foundation.e.apps.utils.enums.User
+import foundation.e.apps.utils.modules.PWAManagerModule
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -65,6 +66,9 @@ class SearchFragment :
@Inject
lateinit var pkgManagerModule: PkgManagerModule
+ @Inject
+ lateinit var pwaManagerModule: PWAManagerModule
+
private var _binding: FragmentSearchBinding? = null
private val binding get() = _binding!!
@@ -119,6 +123,7 @@ class SearchFragment :
fdroidFetchViewModel,
it,
pkgManagerModule,
+ pwaManagerModule,
User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name),
viewLifecycleOwner
) { fusedApp ->
diff --git a/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt b/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt
index 973dfd2e6bb8fef7a2165e596546ef206fa269d1..5685a9e4482b958f87b77a18682f9b8ccb08167c 100644
--- a/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt
+++ b/app/src/main/java/foundation/e/apps/updates/UpdatesFragment.kt
@@ -43,6 +43,7 @@ import foundation.e.apps.manager.download.data.DownloadProgress
import foundation.e.apps.manager.pkg.PkgManagerModule
import foundation.e.apps.utils.enums.Status
import foundation.e.apps.utils.enums.User
+import foundation.e.apps.utils.modules.PWAManagerModule
import kotlinx.coroutines.launch
import javax.inject.Inject
@@ -55,6 +56,9 @@ class UpdatesFragment : Fragment(R.layout.fragment_updates), FusedAPIInterface {
@Inject
lateinit var pkgManagerModule: PkgManagerModule
+ @Inject
+ lateinit var pwaManagerModule: PWAManagerModule
+
private val updatesViewModel: UpdatesViewModel by viewModels()
private val privacyInfoViewModel: PrivacyInfoViewModel by viewModels()
private val fdroidFetchViewModel: FdroidFetchViewModel by viewModels()
@@ -88,6 +92,7 @@ class UpdatesFragment : Fragment(R.layout.fragment_updates), FusedAPIInterface {
fdroidFetchViewModel,
it,
pkgManagerModule,
+ pwaManagerModule,
User.valueOf(mainActivityViewModel.userType.value ?: User.UNAVAILABLE.name),
viewLifecycleOwner,
) { fusedApp ->
diff --git a/app/src/main/java/foundation/e/apps/utils/modules/PWAManagerModule.kt b/app/src/main/java/foundation/e/apps/utils/modules/PWAManagerModule.kt
index 0618a08b25504a16a74030ec61b3a6bbdcb27d54..643298bf88649b13fd01144997bc003c38667e05 100644
--- a/app/src/main/java/foundation/e/apps/utils/modules/PWAManagerModule.kt
+++ b/app/src/main/java/foundation/e/apps/utils/modules/PWAManagerModule.kt
@@ -1,90 +1,147 @@
-package foundation.e.apps.utils.modules
-
-import android.content.ContentUris
-import android.content.ContentValues
-import android.content.Context
-import android.content.Intent
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import android.net.Uri
-import android.util.Base64
-import androidx.core.content.pm.ShortcutInfoCompat
-import androidx.core.content.pm.ShortcutManagerCompat
-import androidx.core.graphics.drawable.IconCompat
-import dagger.hilt.android.qualifiers.ApplicationContext
-import foundation.e.apps.manager.database.DatabaseRepository
-import foundation.e.apps.manager.database.fusedDownload.FusedDownload
-import foundation.e.apps.utils.enums.Status
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class PWAManagerModule @Inject constructor(
- @ApplicationContext private val context: Context,
- private val databaseRepository: DatabaseRepository
-) {
-
- companion object {
- private const val URL = "URL"
- private const val SHORTCUT_ID = "SHORTCUT_ID"
- private const val TITLE = "TITLE"
- private const val ICON = "ICON"
-
- 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"
- private const val VIEW_PWA = "foundation.e.blisslauncher.VIEW_PWA"
- }
-
- suspend fun installPWAApp(fusedDownload: FusedDownload) {
- // Update status
- fusedDownload.status = Status.DOWNLOADING
- databaseRepository.updateDownload(fusedDownload)
-
- // Get bitmap and byteArray for icon
- val iconByteArray = Base64.decode(fusedDownload.iconByteArray, Base64.DEFAULT)
- val iconBitmap = BitmapFactory.decodeByteArray(iconByteArray, 0, iconByteArray.size)
-
- val values = ContentValues()
- values.apply {
- put(URL, fusedDownload.downloadURLList[0])
- put(SHORTCUT_ID, fusedDownload.id)
- put(TITLE, fusedDownload.name)
- put(ICON, iconByteArray)
- }
-
- context.contentResolver.insert(Uri.parse(PWA_PLAYER), values)?.let {
- val databaseID = ContentUris.parseId(it)
- publishShortcut(fusedDownload, iconBitmap, databaseID)
- }
- }
-
- private suspend fun publishShortcut(fusedDownload: FusedDownload, bitmap: Bitmap, databaseID: Long) {
- // Update status
- fusedDownload.status = Status.INSTALLING
- databaseRepository.updateDownload(fusedDownload)
-
- val intent = Intent().apply {
- action = VIEW_PWA
- data = Uri.parse(fusedDownload.downloadURLList[0])
- putExtra(PWA_NAME, fusedDownload.name)
- putExtra(PWA_ID, databaseID)
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS)
- }
-
- val shortcutInfo = ShortcutInfoCompat.Builder(context, fusedDownload.id)
- .setShortLabel(fusedDownload.name)
- .setIcon(IconCompat.createWithBitmap(bitmap))
- .setIntent(intent)
- .build()
- ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)
-
- // Update status
- fusedDownload.status = Status.INSTALLED
- databaseRepository.updateDownload(fusedDownload)
-
- databaseRepository.deleteDownload(fusedDownload)
- }
-}
+package foundation.e.apps.utils.modules
+
+import android.content.ContentUris
+import android.content.ContentValues
+import android.content.Context
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.net.Uri
+import android.util.Base64
+import androidx.core.content.pm.ShortcutInfoCompat
+import androidx.core.content.pm.ShortcutManagerCompat
+import androidx.core.graphics.drawable.IconCompat
+import dagger.hilt.android.qualifiers.ApplicationContext
+import foundation.e.apps.api.fused.data.FusedApp
+import foundation.e.apps.manager.database.DatabaseRepository
+import foundation.e.apps.manager.database.fusedDownload.FusedDownload
+import foundation.e.apps.utils.enums.Status
+import kotlinx.coroutines.delay
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class PWAManagerModule @Inject constructor(
+ @ApplicationContext private val context: Context,
+ private val databaseRepository: DatabaseRepository
+) {
+
+ companion object {
+ private const val URL = "URL"
+ private const val SHORTCUT_ID = "SHORTCUT_ID"
+ private const val TITLE = "TITLE"
+ private const val ICON = "ICON"
+
+ 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"
+ private const val VIEW_PWA = "foundation.e.blisslauncher.VIEW_PWA"
+ }
+
+ /**
+ * Fetch info from PWA Player to check if a PWA is installed.
+ * The column names returned from PWA helper are: [_id, shortcutId, url, title, icon]
+ * The last column ("icon") is a blob.
+ * Note that there is no pwa version. Also there is no "package_name".
+ *
+ * In this method, we get all the available PWAs from PWA Player and compare each of their url
+ * to the method argument [fusedApp]'s url. If an item (from the cursor) has url equal to
+ * that of pwa app, we return [Status.INSTALLED].
+ * We also set [FusedApp.pwaPlayerDbId] for the [fusedApp].
+ *
+ * As there is no concept of version, we cannot send [Status.UPDATABLE].
+ */
+ fun getPwaStatus(fusedApp: FusedApp): Status {
+ context.contentResolver.query(Uri.parse(PWA_PLAYER),
+ null, null, null, null)?.let { cursor ->
+ if (cursor.count > 0) {
+ if (cursor.moveToFirst()) {
+ do {
+ try {
+ val pwaItemUrl = cursor.getString(cursor.columnNames.indexOf("url"))
+ val pwaItemDbId = cursor.getLong(cursor.columnNames.indexOf("_id"))
+ if (fusedApp.url == pwaItemUrl) {
+ fusedApp.pwaPlayerDbId = pwaItemDbId
+ return Status.INSTALLED
+ }
+ }
+ catch (e: Exception) {
+ e.printStackTrace()
+ }
+ } while (cursor.moveToNext())
+ }
+ }
+ cursor.close()
+ }
+
+ return Status.UNAVAILABLE
+ }
+
+ /**
+ * Launch PWA using PWA Player.
+ */
+ fun launchPwa(fusedApp: FusedApp) {
+ val launchIntent = Intent(VIEW_PWA).apply {
+ data = Uri.parse(fusedApp.url)
+ putExtra(PWA_ID, fusedApp.pwaPlayerDbId)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS)
+ }
+ context.startActivity(launchIntent)
+ }
+
+ suspend fun installPWAApp(fusedDownload: FusedDownload) {
+ // Update status
+ fusedDownload.status = Status.DOWNLOADING
+ databaseRepository.updateDownload(fusedDownload)
+
+ // Get bitmap and byteArray for icon
+ val iconByteArray = Base64.decode(fusedDownload.iconByteArray, Base64.DEFAULT)
+ val iconBitmap = BitmapFactory.decodeByteArray(iconByteArray, 0, iconByteArray.size)
+
+ val values = ContentValues()
+ values.apply {
+ put(URL, fusedDownload.downloadURLList[0])
+ put(SHORTCUT_ID, fusedDownload.id)
+ put(TITLE, fusedDownload.name)
+ put(ICON, iconByteArray)
+ }
+
+ context.contentResolver.insert(Uri.parse(PWA_PLAYER), values)?.let {
+ val databaseID = ContentUris.parseId(it)
+ publishShortcut(fusedDownload, iconBitmap, databaseID)
+ }
+ }
+
+ private suspend fun publishShortcut(fusedDownload: FusedDownload, bitmap: Bitmap, databaseID: Long) {
+ // Update status
+ fusedDownload.status = Status.INSTALLING
+ databaseRepository.updateDownload(fusedDownload)
+
+ val intent = Intent().apply {
+ action = VIEW_PWA
+ data = Uri.parse(fusedDownload.downloadURLList[0])
+ putExtra(PWA_NAME, fusedDownload.name)
+ putExtra(PWA_ID, databaseID)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT or Intent.FLAG_ACTIVITY_RETAIN_IN_RECENTS)
+ }
+
+ val shortcutInfo = ShortcutInfoCompat.Builder(context, fusedDownload.id)
+ .setShortLabel(fusedDownload.name)
+ .setIcon(IconCompat.createWithBitmap(bitmap))
+ .setIntent(intent)
+ .build()
+ ShortcutManagerCompat.requestPinShortcut(context, shortcutInfo, null)
+
+ // Add a small delay to avoid conflict of button states.
+ delay(100)
+
+ // Update status
+ fusedDownload.status = Status.INSTALLED
+ databaseRepository.updateDownload(fusedDownload)
+
+ databaseRepository.deleteDownload(fusedDownload)
+ }
+}