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) + } +}