diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9e04f9535..f38363ebb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -92,43 +92,6 @@
android:name=".MainActivity"
android:launchMode="singleTask" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/java/com/aurora/extensions/Context.kt b/app/src/main/java/com/aurora/extensions/Context.kt
index 20b8c4d90..ab56cbdc0 100644
--- a/app/src/main/java/com/aurora/extensions/Context.kt
+++ b/app/src/main/java/com/aurora/extensions/Context.kt
@@ -37,13 +37,13 @@ import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
+import androidx.navigation.NavDeepLinkBuilder
import com.aurora.Constants
import com.aurora.gplayapi.data.models.App
import com.aurora.store.MainActivity
import com.aurora.store.R
import com.aurora.store.util.Log
import com.aurora.store.util.Preferences
-import com.aurora.store.view.ui.details.AppDetailsActivity
import kotlin.system.exitProcess
val Context.inflater: LayoutInflater
@@ -58,12 +58,11 @@ fun Context.browse(url: String, showOpenInAuroraAction: Boolean = false) {
if (showOpenInAuroraAction) {
val icon =
ContextCompat.getDrawable(this, R.drawable.ic_open_in_new)?.toBitmap()
- val pendingIntent = PendingIntent.getActivity(
- this,
- 0,
- Intent(this, AppDetailsActivity::class.java),
- PendingIntent.FLAG_MUTABLE
- )
+ val pendingIntent = NavDeepLinkBuilder(this)
+ .setGraph(R.navigation.mobile_navigation)
+ .setDestination(R.id.appDetailsFragment)
+ .setComponentName(MainActivity::class.java)
+ .createPendingIntent()
customTabsIntent.setActionButton(
icon!!,
this.getString(R.string.open_in_aurora),
diff --git a/app/src/main/java/com/aurora/store/data/service/NotificationService.kt b/app/src/main/java/com/aurora/store/data/service/NotificationService.kt
index 81db411ee..d05982de7 100644
--- a/app/src/main/java/com/aurora/store/data/service/NotificationService.kt
+++ b/app/src/main/java/com/aurora/store/data/service/NotificationService.kt
@@ -29,6 +29,7 @@ import android.util.ArrayMap
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
+import androidx.core.os.bundleOf
import androidx.navigation.NavDeepLinkBuilder
import com.aurora.Constants
import com.aurora.extensions.getStyledAttributeColor
@@ -46,7 +47,6 @@ import com.aurora.store.data.receiver.DownloadResumeReceiver
import com.aurora.store.data.receiver.InstallReceiver
import com.aurora.store.util.CommonUtil
import com.aurora.store.util.Log
-import com.aurora.store.view.ui.details.AppDetailsActivity
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.tonyodev.fetch2.*
@@ -313,17 +313,12 @@ class NotificationService : Service() {
}
private fun getContentIntentForDetails(app: App?): PendingIntent {
- val intent = Intent(this, AppDetailsActivity::class.java)
- intent.putExtra(Constants.STRING_EXTRA, gson.toJson(app))
- val flags = if (isMAndAbove())
- PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE
- else PendingIntent.FLAG_CANCEL_CURRENT
- return PendingIntent.getActivity(
- this,
- packageName.hashCode(),
- intent,
- flags
- )
+ return NavDeepLinkBuilder(this)
+ .setGraph(R.navigation.mobile_navigation)
+ .setDestination(R.id.appDetailsFragment)
+ .setComponentName(MainActivity::class.java)
+ .setArguments(bundleOf("packageName" to app!!.packageName))
+ .createPendingIntent()
}
private fun getContentIntentForDownloads(): PendingIntent {
diff --git a/app/src/main/java/com/aurora/store/util/NavigationUtil.kt b/app/src/main/java/com/aurora/store/util/NavigationUtil.kt
index 87b4f23e8..109c8aabd 100644
--- a/app/src/main/java/com/aurora/store/util/NavigationUtil.kt
+++ b/app/src/main/java/com/aurora/store/util/NavigationUtil.kt
@@ -26,7 +26,6 @@ import androidx.appcompat.app.AppCompatActivity
import com.aurora.Constants
import com.aurora.gplayapi.data.models.App
import com.aurora.store.data.model.Report
-import com.aurora.store.view.ui.details.AppDetailsActivity
import com.aurora.store.view.ui.details.DetailsExodusActivity
import com.aurora.store.view.ui.details.DevAppsActivity
import com.google.gson.Gson
@@ -36,17 +35,6 @@ import java.lang.reflect.Modifier
object NavigationUtil {
val gson: Gson = GsonBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT).create()
- fun openDetailsActivity(context: Context, app: App) {
- val intent = Intent(
- context,
- AppDetailsActivity::class.java
- ).apply {
- putExtra(Constants.STRING_EXTRA, gson.toJson(app))
- }
- val options = ActivityOptions.makeSceneTransitionAnimation(context as AppCompatActivity)
- context.startActivity(intent, options.toBundle())
- }
-
fun openDevAppsActivity(context: Context, app: App) {
val intent = Intent(
context,
diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/BaseActivity.kt b/app/src/main/java/com/aurora/store/view/ui/commons/BaseActivity.kt
index a6b81a32f..f7fde043f 100644
--- a/app/src/main/java/com/aurora/store/view/ui/commons/BaseActivity.kt
+++ b/app/src/main/java/com/aurora/store/view/ui/commons/BaseActivity.kt
@@ -21,7 +21,6 @@ package com.aurora.store.view.ui.commons
import android.app.ActivityOptions
import android.content.Intent
-import android.os.Build
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.aurora.Constants
@@ -38,10 +37,7 @@ import com.aurora.store.view.ui.sheets.NetworkDialogSheet
import com.aurora.store.view.ui.sheets.TOSSheet
import com.google.gson.Gson
import com.google.gson.GsonBuilder
-import nl.komponents.kovenant.task
-import nl.komponents.kovenant.ui.successUi
import java.lang.reflect.Modifier
-import java.util.concurrent.TimeUnit
abstract class BaseActivity : AppCompatActivity(), NetworkProvider.NetworkListener {
@@ -56,7 +52,7 @@ abstract class BaseActivity : AppCompatActivity(), NetworkProvider.NetworkListen
}
fun openDetailsActivity(app: App) {
- val intent = Intent(this, AppDetailsActivity::class.java)
+ val intent = Intent(this, AppDetailsFragment::class.java)
intent.putExtra(Constants.STRING_EXTRA, gson.toJson(app))
val options = ActivityOptions.makeSceneTransitionAnimation(this)
startActivity(intent, options.toBundle())
diff --git a/app/src/main/java/com/aurora/store/view/ui/commons/BaseFragment.kt b/app/src/main/java/com/aurora/store/view/ui/commons/BaseFragment.kt
index 5d4b2a4cc..86aad51f9 100644
--- a/app/src/main/java/com/aurora/store/view/ui/commons/BaseFragment.kt
+++ b/app/src/main/java/com/aurora/store/view/ui/commons/BaseFragment.kt
@@ -23,11 +23,12 @@ import android.app.ActivityOptions
import android.content.Intent
import androidx.annotation.LayoutRes
import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
import com.aurora.Constants
import com.aurora.extensions.getEmptyActivityBundle
import com.aurora.gplayapi.data.models.App
import com.aurora.gplayapi.data.models.Category
-import com.aurora.store.view.ui.details.AppDetailsActivity
+import com.aurora.store.MobileNavigationDirections
import com.aurora.store.view.ui.details.DevProfileActivity
import com.google.gson.Gson
import com.google.gson.GsonBuilder
@@ -44,10 +45,9 @@ open class BaseFragment : Fragment {
).create()
fun openDetailsActivity(app: App) {
- val intent = Intent(context, AppDetailsActivity::class.java)
- intent.putExtra(Constants.STRING_EXTRA, Gson().toJson(app))
- val options = ActivityOptions.makeSceneTransitionAnimation(requireActivity())
- startActivity(intent, options.toBundle())
+ findNavController().navigate(
+ MobileNavigationDirections.actionGlobalAppDetailsFragment(app.packageName)
+ )
}
fun openCategoryBrowseActivity(category: Category) {
diff --git a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt
deleted file mode 100644
index 27b1e0488..000000000
--- a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsActivity.kt
+++ /dev/null
@@ -1,846 +0,0 @@
-/*
- * Aurora Store
- * Copyright (C) 2021, Rahul Kumar Patel
- * Copyright (C) 2022, The Calyx Institute
- *
- * Aurora Store 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 2 of the License, or
- * (at your option) any later version.
- *
- * Aurora Store 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 Aurora Store. If not, see .
- *
- */
-
-package com.aurora.store.view.ui.details
-
-import android.Manifest
-import android.content.ActivityNotFoundException
-import android.content.ComponentName
-import android.content.Intent
-import android.content.ServiceConnection
-import android.content.pm.PackageManager
-import android.os.Build
-import android.os.Bundle
-import android.os.Environment
-import android.os.IBinder
-import android.provider.Settings
-import android.view.Menu
-import android.view.MenuItem
-import android.view.View
-import android.widget.LinearLayout
-import androidx.activity.addCallback
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.core.content.ContextCompat
-import com.aurora.Constants
-import com.aurora.extensions.*
-import com.aurora.gplayapi.data.models.App
-import com.aurora.gplayapi.data.models.AuthData
-import com.aurora.gplayapi.helpers.AppDetailsHelper
-import com.aurora.store.MainActivity
-import com.aurora.store.R
-import com.aurora.store.State
-import com.aurora.store.data.downloader.DownloadManager
-import com.aurora.store.data.downloader.getGroupId
-import com.aurora.store.data.event.BusEvent
-import com.aurora.store.data.event.InstallerEvent
-import com.aurora.store.data.installer.AppInstaller
-import com.aurora.store.data.network.HttpClient
-import com.aurora.store.data.providers.AuthProvider
-import com.aurora.store.data.service.AppMetadataStatusListener
-import com.aurora.store.data.service.UpdateService
-import com.aurora.store.databinding.ActivityDetailsBinding
-import com.aurora.store.util.*
-import com.aurora.store.view.ui.sheets.InstallErrorDialogSheet
-import com.aurora.store.view.ui.sheets.ManualDownloadSheet
-import com.bumptech.glide.load.resource.bitmap.RoundedCorners
-import com.google.android.material.bottomsheet.BottomSheetBehavior
-import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
-import com.tonyodev.fetch2.*
-import com.tonyodev.fetch2core.DownloadBlock
-import nl.komponents.kovenant.task
-import nl.komponents.kovenant.ui.failUi
-import nl.komponents.kovenant.ui.successUi
-import org.greenrobot.eventbus.EventBus
-import org.greenrobot.eventbus.Subscribe
-import org.greenrobot.eventbus.ThreadMode
-import java.io.File
-
-class AppDetailsActivity : BaseDetailsActivity() {
-
- private lateinit var B: ActivityDetailsBinding
- private lateinit var bottomSheetBehavior: BottomSheetBehavior
-
- private val startForStorageManagerResult =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
- if (isRAndAbove() && Environment.isExternalStorageManager()) {
- updateApp(app)
- } else {
- toast(R.string.permissions_denied)
- }
- }
- private val startForPermissions =
- registerForActivityResult(ActivityResultContracts.RequestPermission()) {
- if (it) updateApp(app) else toast(R.string.permissions_denied)
- }
-
- private lateinit var authData: AuthData
- private lateinit var app: App
- private var fetch: Fetch? = null
- private var downloadManager: DownloadManager? = null
-
- private var attachToServiceCalled = false
- private var updateService: UpdateService? = null
- private var pendingAddListeners = true
- private var serviceConnection = object : ServiceConnection {
- override fun onServiceConnected(name: ComponentName, binder: IBinder) {
- updateService = (binder as UpdateService.UpdateServiceBinder).getUpdateService()
- if (::fetchGroupListener.isInitialized && ::appMetadataListener.isInitialized && pendingAddListeners) {
- updateService!!.registerFetchListener(fetchGroupListener)
- // appMetadataListener needs to be initialized after the fetchGroupListener
- updateService!!.registerAppMetadataListener(appMetadataListener)
- pendingAddListeners = false
- }
- if (listOfActionsWhenServiceAttaches.isNotEmpty()) {
- val iterator = listOfActionsWhenServiceAttaches.iterator()
- while (iterator.hasNext()) {
- val next = iterator.next()
- next.run()
- iterator.remove()
- }
- }
- }
-
- override fun onServiceDisconnected(name: ComponentName) {
- updateService = null
- attachToServiceCalled = false
- pendingAddListeners = true
- }
- }
- private lateinit var fetchGroupListener: FetchGroupListener
- private lateinit var appMetadataListener: AppMetadataStatusListener
- private lateinit var completionMarker: java.io.File
- private lateinit var inProgressMarker: java.io.File
-
- private var isExternal = false
- private var isNone = false
- private var status = Status.NONE
- private var isInstalled: Boolean = false
- private var isUpdatable: Boolean = false
- private var autoDownload: Boolean = false
- private var downloadOnly: Boolean = false
-
- override fun onConnected() {
-
- }
-
- override fun onDisconnected() {
-
- }
-
- override fun onReconnected() {
-
- }
-
- override fun onStart() {
- super.onStart()
- EventBus.getDefault().register(this)
- if (autoDownload) {
- purchase()
- }
- }
-
- override fun onStop() {
- EventBus.getDefault().unregister(this)
- super.onStop()
- }
-
- @Subscribe(threadMode = ThreadMode.MAIN)
- fun onEventMainThread(event: Any) {
- when (event) {
- is BusEvent.InstallEvent -> {
- if (app.packageName == event.packageName) {
- attachActions()
- }
- }
- is BusEvent.UninstallEvent -> {
- if (app.packageName == event.packageName) {
- attachActions()
- }
- }
- is BusEvent.ManualDownload -> {
- if (app.packageName == event.packageName) {
- app.versionCode = event.versionCode
- purchase()
- }
- }
- is InstallerEvent.Failed -> {
- if (app.packageName == event.packageName) {
- InstallErrorDialogSheet.newInstance(
- app,
- event.packageName,
- event.error,
- event.extra
- ).show(supportFragmentManager, "SED")
- attachActions()
- updateActionState(State.IDLE)
- }
- }
- else -> {
-
- }
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- B = ActivityDetailsBinding.inflate(layoutInflater)
- setContentView(B.root)
-
- onNewIntent(intent)
-
- onBackPressedDispatcher.addCallback(this) {
- if (isExternal) {
- open(MainActivity::class.java, true)
- } else {
- finish()
- }
- }
- }
-
- override fun onResume() {
- getUpdateServiceInstance()
- checkAndSetupInstall()
- super.onResume()
- }
-
- override fun onNewIntent(intent: Intent) {
- super.onNewIntent(intent)
-
- if (intent.scheme != null && (intent.scheme == "market" || intent.scheme == "http" || intent.scheme == "https")) {
- val packageName = intent.data!!.getQueryParameter("id")
- val packageVersion = intent.data!!.getQueryParameter("v")
- if (packageName.isNullOrEmpty()) {
- finishAfterTransition()
- } else {
- isExternal = true
- app = App(packageName)
- if (!packageVersion.isNullOrEmpty()) {
- app.versionCode = packageVersion.toInt()
- }
- fetchCompleteApp()
- }
- } else {
- val rawApp: String? = intent.getStringExtra(Constants.STRING_EXTRA)
- if (rawApp != null) {
- app = gson.fromJson(rawApp, App::class.java)
- isInstalled = PackageUtil.isInstalled(this, app.packageName)
-
- inflatePartialApp()
- fetchCompleteApp()
- } else {
- finishAfterTransition()
- }
- }
- }
-
- private var uninstallActionEnabled = false
-
- override fun onCreateOptionsMenu(menu: Menu?): Boolean {
- menuInflater.inflate(R.menu.menu_details, menu)
- if (::app.isInitialized) {
- val installed = PackageUtil.isInstalled(this, app.packageName)
- menu?.findItem(R.id.action_uninstall)?.isVisible = installed
- uninstallActionEnabled = installed
- }
- return true
- }
-
- override fun onOptionsItemSelected(item: MenuItem): Boolean {
- when (item.itemId) {
- android.R.id.home -> {
- onBackPressedDispatcher.onBackPressed()
- return true
- }
- R.id.action_share -> {
- share(app)
- return true
- }
- R.id.action_uninstall -> {
- uninstallApp()
- return true
- }
- R.id.menu_download_manual -> {
- val sheet = ManualDownloadSheet.newInstance(app)
- sheet.isCancelable = false
- sheet.show(supportFragmentManager, ManualDownloadSheet.TAG)
- return true
- }
- R.id.action_playstore -> {
- browse("${Constants.SHARE_URL}${app.packageName}")
- return true
- }
- }
- return super.onOptionsItemSelected(item)
- }
-
- private fun attachToolbar() {
- setSupportActionBar(B.layoutDetailsToolbar.toolbar)
- val actionBar = supportActionBar
- if (actionBar != null) {
- actionBar.setDisplayShowCustomEnabled(true)
- actionBar.setDisplayHomeAsUpEnabled(true)
- actionBar.elevation = 0f
- actionBar.title = ""
- }
- }
-
- private fun attachActions() {
- flip(0)
- checkAndSetupInstall()
- }
-
- private fun updateActionState(state: State) {
- B.layoutDetailsInstall.btnDownload.updateState(state)
- }
-
- private fun openApp() {
- val intent = PackageUtil.getLaunchIntent(this, app.packageName)
- if (intent != null) {
- try {
- startActivity(intent)
- } catch (e: ActivityNotFoundException) {
- toast("Unable to open app")
- }
- }
- }
-
- private fun verifyAndInstall(files: List) {
- if (downloadOnly)
- return
-
- var filesExist = true
-
- files.forEach { download ->
- filesExist = filesExist && File(download.file).exists()
- }
-
- if (filesExist)
- install(files)
- else
- purchase()
- }
-
- @Synchronized
- private fun install(files: List) {
- updateActionState(State.IDLE)
-
- val apkFiles = files.filter { it.file.endsWith(".apk") }
- val preferredInstaller = Preferences.getInteger(this, Preferences.PREFERENCE_INSTALLER_ID)
-
- if (apkFiles.size > 1 && preferredInstaller == 1) {
- showDialog(R.string.title_installer, R.string.dialog_desc_native_split)
- } else {
- task {
- AppInstaller.getInstance(this)
- .getPreferredInstaller()
- .install(
- app.packageName,
- apkFiles.map { it.file }
- )
- } fail {
- Log.e(it.stackTraceToString())
- }
-
- runOnUiThread {
- B.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing))
- }
- }
- }
-
- @Synchronized
- private fun uninstallApp() {
- task {
- AppInstaller.getInstance(this)
- .getPreferredInstaller()
- .uninstall(app.packageName)
- }
- }
-
- private fun attachWhiteListStatus() {
-
- }
-
- private fun fetchCompleteApp() {
- task {
- authData = AuthProvider.with(this).getAuthData()
- return@task AppDetailsHelper(authData)
- .using(HttpClient.getPreferredClient())
- .getAppByPackageName(app.packageName)
- } successUi {
- if (isExternal) {
- app = it
- inflatePartialApp()
- }
- inflateExtraDetails(it)
- } failUi {
- toast("Failed to fetch app details")
- }
- }
-
- private fun inflatePartialApp() {
- if (::app.isInitialized) {
- attachWhiteListStatus()
- attachHeader()
- attachToolbar()
- attachBottomSheet()
- attachFetch()
- attachActions()
-
- if (autoDownload) {
- purchase()
- }
- }
- }
-
- private fun attachHeader() {
- B.layoutDetailsApp.apply {
- imgIcon.load(app.iconArtwork.url) {
- placeholder(R.drawable.bg_placeholder)
- transform(RoundedCorners(32))
- }
-
- txtLine1.text = app.displayName
- txtLine2.text = app.developerName
- txtLine2.setOnClickListener {
- NavigationUtil.openDevAppsActivity(
- this@AppDetailsActivity,
- app
- )
- }
- txtLine3.text = ("${app.versionName} (${app.versionCode})")
- packageName.text = app.packageName
-
- val tags = mutableListOf()
- if (app.isFree)
- tags.add(getString(R.string.details_free))
- else
- tags.add(getString(R.string.details_paid))
-
- if (app.containsAds)
- tags.add(getString(R.string.details_contains_ads))
- else
- tags.add(getString(R.string.details_no_ads))
-
- txtLine4.text = tags.joinToString(separator = " • ")
- }
- }
-
- private fun inflateExtraDetails(app: App?) {
- app?.let {
- B.viewFlipper.displayedChild = 1
- inflateAppDescription(B.layoutDetailDescription, app)
- inflateAppRatingAndReviews(B.layoutDetailsReview, app)
- inflateAppDevInfo(B.layoutDetailsDev, app)
- inflateAppPrivacy(B.layoutDetailsPrivacy, app)
- inflateAppPermission(B.layoutDetailsPermissions, app)
-
- if (!authData.isAnonymous) {
- app.testingProgram?.let {
- if (it.isAvailable && it.isSubscribed) {
- B.layoutDetailsApp.txtLine1.text = it.displayName
- }
- }
-
- inflateBetaSubscription(B.layoutDetailsBeta, app)
- }
-
- if (Preferences.getBoolean(this, Preferences.PREFERENCE_SIMILAR)) {
- inflateAppStream(B.epoxyRecyclerStream, app)
- }
- }
- }
-
- @Synchronized
- private fun startDownload() {
- when (status) {
- Status.PAUSED -> {
- fetch?.resumeGroup(app.getGroupId(this@AppDetailsActivity))
- }
- Status.DOWNLOADING -> {
- flip(1)
- toast("Already downloading")
- }
- Status.COMPLETED -> {
- fetch?.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) {
- verifyAndInstall(it.downloads)
- }
- }
- else -> {
- purchase()
- }
- }
- }
-
- val listOfActionsWhenServiceAttaches = ArrayList()
-
- private fun purchase() {
- bottomSheetBehavior.isHideable = false
- bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
- updateActionState(State.PROGRESS)
-
- if (PathUtil.needsStorageManagerPerm(app.fileList) || this.isExternalStorageEnable()) {
- if (isRAndAbove()) {
- if (!Environment.isExternalStorageManager()) {
- startForStorageManagerResult.launch(
- Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
- )
- } else {
- updateApp(app)
- }
- } else {
- if (ContextCompat.checkSelfPermission(
- this,
- Manifest.permission.WRITE_EXTERNAL_STORAGE
- ) == PackageManager.PERMISSION_GRANTED
- ) {
- updateApp(app)
- } else {
- startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
- }
- }
- } else {
- updateApp(app)
- }
- }
-
- private fun updateApp(app: App) {
- if (updateService == null) {
- listOfActionsWhenServiceAttaches.add {
- updateService?.updateApp(app, true)
- }
- getUpdateServiceInstance()
- } else {
- updateService?.updateApp(app, true)
- }
- }
-
- private fun updateProgress(
- fetchGroup: FetchGroup,
- etaInMilliSeconds: Long,
- downloadedBytesPerSecond: Long
- ) {
- runOnUiThread {
- val progress = if (fetchGroup.groupDownloadProgress > 0)
- fetchGroup.groupDownloadProgress
- else
- 0
-
- if (progress == 100) {
- B.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing))
- return@runOnUiThread
- }
- B.layoutDetailsInstall.apply {
- txtProgressPercent.text = ("${progress}%")
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- progressDownload.setProgress(progress, true)
- } else {
- progressDownload.progress = progress
- }
-
- txtEta.text = CommonUtil.getETAString(
- this@AppDetailsActivity,
- etaInMilliSeconds
- )
- txtSpeed.text =
- CommonUtil.getDownloadSpeedString(
- this@AppDetailsActivity,
- downloadedBytesPerSecond
- )
- }
- }
- }
-
- private fun expandBottomSheet(message: String?) {
- bottomSheetBehavior.isHideable = false
- bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
-
- with(B.layoutDetailsInstall) {
- txtPurchaseError.text = message
- btnDownload.updateState(State.IDLE)
- if (app.isFree)
- btnDownload.setText(R.string.action_install)
- else
- btnDownload.setText(app.price)
- }
- }
-
- private fun checkAndSetupInstall() {
- isInstalled = PackageUtil.isInstalled(this, app.packageName)
-
- B.layoutDetailsInstall.btnDownload.let { btn ->
- if (isInstalled) {
- isUpdatable = PackageUtil.isUpdatable(
- this,
- app.packageName,
- app.versionCode.toLong()
- )
-
- val installedVersion = PackageUtil.getInstalledVersion(this, app.packageName)
-
- if (isUpdatable) {
- B.layoutDetailsApp.txtLine3.text =
- ("$installedVersion ➔ ${app.versionName} (${app.versionCode})")
- btn.setText(R.string.action_update)
- btn.addOnClickListener { startDownload() }
- } else {
- B.layoutDetailsApp.txtLine3.text = installedVersion
- btn.setText(R.string.action_open)
- btn.addOnClickListener { openApp() }
- }
- if (!uninstallActionEnabled) {
- invalidateOptionsMenu()
- }
- } else {
- if (app.isFree) {
- btn.setText(R.string.action_install)
- } else {
- btn.setText(app.price)
- }
-
- btn.addOnClickListener {
- if (authData.isAnonymous && !app.isFree) {
- toast(R.string.toast_purchase_blocked)
- } else {
- btn.setText(R.string.download_metadata)
- startDownload()
- }
- }
- if (uninstallActionEnabled) {
- invalidateOptionsMenu()
- }
- }
- }
- }
-
- @Synchronized
- private fun flip(nextView: Int) {
- runOnUiThread {
- val displayChild = B.layoutDetailsInstall.viewFlipper.displayedChild
- if (displayChild != nextView) {
- B.layoutDetailsInstall.viewFlipper.displayedChild = nextView
- if (nextView == 0)
- checkAndSetupInstall()
- }
- }
- }
-
- private fun attachFetch() {
- if (fetch == null) {
- downloadManager = DownloadManager.with(this)
- fetch = downloadManager!!.fetch
- }
- fetch?.getFetchGroup(app.getGroupId(this@AppDetailsActivity)) { fetchGroup: FetchGroup ->
- if (fetchGroup.groupDownloadProgress == 100 && fetchGroup.completedDownloads.isNotEmpty()) {
- status = Status.COMPLETED
- } else if (downloadManager?.isDownloading(fetchGroup) == true) {
- status = Status.DOWNLOADING
- flip(1)
- } else if (downloadManager?.isCanceled(fetchGroup) == true) {
- status = Status.CANCELLED
- } else if (fetchGroup.pausedDownloads.isNotEmpty()) {
- status = Status.PAUSED
- } else {
- status = Status.NONE
- }
- }
-
- fetchGroupListener = object : AbstractFetchGroupListener() {
-
- override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) {
- if (groupId == app.getGroupId(this@AppDetailsActivity)) {
- status = download.status
- }
- }
-
- override fun onStarted(
- groupId: Int,
- download: Download,
- downloadBlocks: List,
- totalBlocks: Int,
- fetchGroup: FetchGroup
- ) {
- if (groupId == app.getGroupId(this@AppDetailsActivity)) {
- status = download.status
- flip(1)
-
- val pkgDir = PathUtil.getPackageDirectory(applicationContext, app.packageName)
- completionMarker =
- java.io.File("$pkgDir/.${app.versionCode}.download-complete")
- inProgressMarker =
- java.io.File("$pkgDir/.${app.versionCode}.download-in-progress")
-
- if (completionMarker.exists())
- completionMarker.delete()
-
- inProgressMarker.createNewFile()
- }
- }
-
- override fun onResumed(groupId: Int, download: Download, fetchGroup: FetchGroup) {
- if (groupId == app.getGroupId(this@AppDetailsActivity)) {
- status = download.status
- flip(1)
- inProgressMarker.parentFile?.mkdirs()
- inProgressMarker.createNewFile()
- }
- }
-
- override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) {
- if (groupId == app.getGroupId(this@AppDetailsActivity)) {
- status = download.status
- flip(0)
- }
- }
-
- override fun onProgress(
- groupId: Int,
- download: Download,
- etaInMilliSeconds: Long,
- downloadedBytesPerSecond: Long,
- fetchGroup: FetchGroup
- ) {
- if (groupId == app.getGroupId(this@AppDetailsActivity)) {
- updateProgress(fetchGroup, etaInMilliSeconds, downloadedBytesPerSecond)
- Log.i(
- "${app.displayName} : ${download.file} -> Progress : %d",
- fetchGroup.groupDownloadProgress
- )
- }
- }
-
- override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
- if (groupId == app.getGroupId(this@AppDetailsActivity) && fetchGroup.groupDownloadProgress == 100) {
- status = download.status
- flip(0)
- updateProgress(fetchGroup, -1, -1)
- try {
- inProgressMarker.delete()
- completionMarker.createNewFile()
- } catch (ex: Exception) {
- ex.printStackTrace()
- }
- }
- }
-
- override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
- if (groupId == app.getGroupId(this@AppDetailsActivity)) {
- status = download.status
- flip(0)
- inProgressMarker.delete()
- }
- }
-
- override fun onError(
- groupId: Int,
- download: Download,
- error: Error,
- throwable: Throwable?,
- fetchGroup: FetchGroup
- ) {
- if (groupId == app.getGroupId(this@AppDetailsActivity)) {
- status = download.status
- flip(0)
- inProgressMarker.delete()
- }
- }
- }
-
- appMetadataListener = object : AppMetadataStatusListener {
- override fun onAppMetadataStatusError(reason: String, app: App) {
- if (app.packageName == this@AppDetailsActivity.app.packageName) {
- updateActionState(State.IDLE)
- expandBottomSheet(reason)
- }
- }
- }
-
- getUpdateServiceInstance()
-
- B.layoutDetailsInstall.imgCancel.setOnClickListener {
- fetch?.cancelGroup(
- app.getGroupId(this@AppDetailsActivity)
- )
- }
- if (updateService != null) {
- pendingAddListeners = false
- updateService!!.registerFetchListener(fetchGroupListener)
- // appMetadataListener needs to be initialized after the fetchGroupListener
- updateService!!.registerAppMetadataListener(appMetadataListener)
- } else {
- pendingAddListeners = true
- }
- }
-
- fun getUpdateServiceInstance() {
- if (updateService == null && !attachToServiceCalled) {
- attachToServiceCalled = true
- val intent = Intent(this, UpdateService::class.java)
- startService(intent)
- bindService(
- intent,
- serviceConnection,
- 0
- )
- }
- }
-
- override fun onPause() {
- if (updateService != null) {
- updateService = null
- attachToServiceCalled = false
- pendingAddListeners = true
- unbindService(serviceConnection)
- }
- super.onPause()
- }
-
- override fun onDestroy() {
- super.onDestroy()
- if (updateService != null) {
- updateService = null
- attachToServiceCalled = false
- pendingAddListeners = true
- unbindService(serviceConnection)
- }
- }
-
- private fun attachBottomSheet() {
- B.layoutDetailsInstall.apply {
- viewFlipper.setInAnimation(this@AppDetailsActivity, R.anim.fade_in)
- viewFlipper.setOutAnimation(this@AppDetailsActivity, R.anim.fade_out)
- }
-
- bottomSheetBehavior = BottomSheetBehavior.from(B.layoutDetailsInstall.bottomSheet)
- bottomSheetBehavior.isDraggable = false
-
- bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetCallback() {
- override fun onStateChanged(bottomSheet: View, newState: Int) {
- if (newState == BottomSheetBehavior.STATE_EXPANDED) {
- bottomSheetBehavior.setDraggable(true)
- } else if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
- bottomSheetBehavior.isDraggable = false
- }
- }
-
- override fun onSlide(bottomSheet: View, slideOffset: Float) {}
- })
- }
-}
diff --git a/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt
new file mode 100644
index 000000000..fe027e390
--- /dev/null
+++ b/app/src/main/java/com/aurora/store/view/ui/details/AppDetailsFragment.kt
@@ -0,0 +1,1283 @@
+/*
+ * Aurora Store
+ * Copyright (C) 2021, Rahul Kumar Patel
+ * Copyright (C) 2022, The Calyx Institute
+ *
+ * Aurora Store 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Aurora Store 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 Aurora Store. If not, see .
+ *
+ */
+
+package com.aurora.store.view.ui.details
+
+import android.Manifest
+import android.content.ActivityNotFoundException
+import android.content.ComponentName
+import android.content.Intent
+import android.content.ServiceConnection
+import android.content.pm.PackageManager
+import android.os.Build
+import android.os.Bundle
+import android.os.Environment
+import android.os.IBinder
+import android.provider.Settings
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.RelativeLayout
+import android.widget.Toast
+import androidx.activity.addCallback
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.ContextCompat
+import androidx.core.text.HtmlCompat
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
+import com.airbnb.epoxy.EpoxyRecyclerView
+import com.aurora.Constants
+import com.aurora.extensions.browse
+import com.aurora.extensions.getString
+import com.aurora.extensions.hide
+import com.aurora.extensions.isRAndAbove
+import com.aurora.extensions.load
+import com.aurora.extensions.runOnUiThread
+import com.aurora.extensions.share
+import com.aurora.extensions.show
+import com.aurora.extensions.showDialog
+import com.aurora.extensions.stackTraceToString
+import com.aurora.extensions.toast
+import com.aurora.gplayapi.data.models.App
+import com.aurora.gplayapi.data.models.AuthData
+import com.aurora.gplayapi.data.models.Review
+import com.aurora.gplayapi.data.models.StreamBundle
+import com.aurora.gplayapi.data.models.StreamCluster
+import com.aurora.gplayapi.helpers.AppDetailsHelper
+import com.aurora.gplayapi.helpers.ReviewsHelper
+import com.aurora.store.MainActivity
+import com.aurora.store.R
+import com.aurora.store.State
+import com.aurora.store.data.ViewState
+import com.aurora.store.data.downloader.DownloadManager
+import com.aurora.store.data.downloader.getGroupId
+import com.aurora.store.data.event.BusEvent
+import com.aurora.store.data.event.InstallerEvent
+import com.aurora.store.data.installer.AppInstaller
+import com.aurora.store.data.model.ExodusReport
+import com.aurora.store.data.model.Report
+import com.aurora.store.data.network.HttpClient
+import com.aurora.store.data.providers.AuthProvider
+import com.aurora.store.data.service.AppMetadataStatusListener
+import com.aurora.store.data.service.UpdateService
+import com.aurora.store.databinding.FragmentDetailsBinding
+import com.aurora.store.databinding.LayoutDetailsBetaBinding
+import com.aurora.store.databinding.LayoutDetailsDescriptionBinding
+import com.aurora.store.databinding.LayoutDetailsDevBinding
+import com.aurora.store.databinding.LayoutDetailsPermissionsBinding
+import com.aurora.store.databinding.LayoutDetailsPrivacyBinding
+import com.aurora.store.databinding.LayoutDetailsReviewBinding
+import com.aurora.store.util.CommonUtil
+import com.aurora.store.util.Log
+import com.aurora.store.util.NavigationUtil
+import com.aurora.store.util.PackageUtil
+import com.aurora.store.util.PathUtil
+import com.aurora.store.util.Preferences
+import com.aurora.store.util.isExternalStorageEnable
+import com.aurora.store.view.custom.RatingView
+import com.aurora.store.view.epoxy.controller.DetailsCarouselController
+import com.aurora.store.view.epoxy.controller.GenericCarouselController
+import com.aurora.store.view.epoxy.views.details.ReviewViewModel_
+import com.aurora.store.view.epoxy.views.details.ScreenshotView
+import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_
+import com.aurora.store.view.ui.commons.BaseFragment
+import com.aurora.store.view.ui.sheets.InstallErrorDialogSheet
+import com.aurora.store.view.ui.sheets.ManualDownloadSheet
+import com.aurora.store.view.ui.sheets.PermissionBottomSheet
+import com.aurora.store.viewmodel.details.DetailsClusterViewModel
+import com.bumptech.glide.load.resource.bitmap.RoundedCorners
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
+import com.tonyodev.fetch2.AbstractFetchGroupListener
+import com.tonyodev.fetch2.Download
+import com.tonyodev.fetch2.Error
+import com.tonyodev.fetch2.Fetch
+import com.tonyodev.fetch2.FetchGroup
+import com.tonyodev.fetch2.FetchGroupListener
+import com.tonyodev.fetch2.Status
+import com.tonyodev.fetch2core.DownloadBlock
+import nl.komponents.kovenant.task
+import nl.komponents.kovenant.ui.failUi
+import nl.komponents.kovenant.ui.successUi
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+import org.json.JSONObject
+import java.io.File
+import java.util.Locale
+
+class AppDetailsFragment : BaseFragment(R.layout.fragment_details) {
+
+ private var _binding: FragmentDetailsBinding? = null
+ private val binding: FragmentDetailsBinding
+ get() = _binding!!
+
+ private val args: AppDetailsFragmentArgs by navArgs()
+
+ private lateinit var bottomSheetBehavior: BottomSheetBehavior
+
+ private val exodusBaseUrl = "https://reports.exodus-privacy.eu.org/api/search/"
+ private val exodusApiKey = "Token bbe6ebae4ad45a9cbacb17d69739799b8df2c7ae"
+
+ private val startForStorageManagerResult =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ if (isRAndAbove() && Environment.isExternalStorageManager()) {
+ updateApp(app)
+ } else {
+ toast(R.string.permissions_denied)
+ }
+ }
+ private val startForPermissions =
+ registerForActivityResult(ActivityResultContracts.RequestPermission()) {
+ if (it) updateApp(app) else toast(R.string.permissions_denied)
+ }
+
+ private lateinit var authData: AuthData
+ private lateinit var app: App
+ private var fetch: Fetch? = null
+ private var downloadManager: DownloadManager? = null
+
+ private var attachToServiceCalled = false
+ private var updateService: UpdateService? = null
+ private var pendingAddListeners = true
+ private var serviceConnection = object : ServiceConnection {
+ override fun onServiceConnected(name: ComponentName, binder: IBinder) {
+ updateService = (binder as UpdateService.UpdateServiceBinder).getUpdateService()
+ if (::fetchGroupListener.isInitialized && ::appMetadataListener.isInitialized && pendingAddListeners) {
+ updateService!!.registerFetchListener(fetchGroupListener)
+ // appMetadataListener needs to be initialized after the fetchGroupListener
+ updateService!!.registerAppMetadataListener(appMetadataListener)
+ pendingAddListeners = false
+ }
+ if (listOfActionsWhenServiceAttaches.isNotEmpty()) {
+ val iterator = listOfActionsWhenServiceAttaches.iterator()
+ while (iterator.hasNext()) {
+ val next = iterator.next()
+ next.run()
+ iterator.remove()
+ }
+ }
+ }
+
+ override fun onServiceDisconnected(name: ComponentName) {
+ updateService = null
+ attachToServiceCalled = false
+ pendingAddListeners = true
+ }
+ }
+ private lateinit var fetchGroupListener: FetchGroupListener
+ private lateinit var appMetadataListener: AppMetadataStatusListener
+ private lateinit var completionMarker: File
+ private lateinit var inProgressMarker: File
+
+ private var isExternal = false
+ private var isNone = false
+ private var status = Status.NONE
+ private var isInstalled: Boolean = false
+ private var isUpdatable: Boolean = false
+ private var autoDownload: Boolean = false
+ private var downloadOnly: Boolean = false
+ private var uninstallActionEnabled = false
+
+ val listOfActionsWhenServiceAttaches = ArrayList()
+
+ override fun onStart() {
+ super.onStart()
+ EventBus.getDefault().register(this)
+ if (autoDownload) {
+ purchase()
+ }
+ }
+
+ override fun onStop() {
+ EventBus.getDefault().unregister(this)
+ super.onStop()
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ fun onEventMainThread(event: Any) {
+ when (event) {
+ is BusEvent.InstallEvent -> {
+ if (app.packageName == event.packageName) {
+ attachActions()
+ }
+ }
+
+ is BusEvent.UninstallEvent -> {
+ if (app.packageName == event.packageName) {
+ attachActions()
+ }
+ }
+
+ is BusEvent.ManualDownload -> {
+ if (app.packageName == event.packageName) {
+ app.versionCode = event.versionCode
+ purchase()
+ }
+ }
+
+ is InstallerEvent.Failed -> {
+ if (app.packageName == event.packageName) {
+ InstallErrorDialogSheet.newInstance(
+ app,
+ event.packageName,
+ event.error,
+ event.extra
+ ).show(childFragmentManager, "SED")
+ attachActions()
+ updateActionState(State.IDLE)
+ }
+ }
+
+ else -> {
+
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ _binding = FragmentDetailsBinding.bind(view)
+
+ // TODO: Parcel app class to better handle onNewIntent fun
+ isExternal = true
+ app = App(args.packageName)
+ fetchCompleteApp()
+
+ // Toolbar
+ binding.layoutDetailsToolbar.toolbar.apply {
+ elevation = 0f
+ navigationIcon = ContextCompat.getDrawable(view.context, R.drawable.ic_arrow_back)
+ setNavigationOnClickListener { findNavController().navigateUp() }
+ inflateMenu(R.menu.menu_details)
+
+ setOnMenuItemClickListener {
+ when (it.itemId) {
+ R.id.action_share -> {
+ view.context.share(app)
+ }
+
+ R.id.action_uninstall -> {
+ uninstallApp()
+ }
+
+ R.id.menu_download_manual -> {
+ val sheet = ManualDownloadSheet.newInstance(app)
+ sheet.isCancelable = false
+ sheet.show(childFragmentManager, ManualDownloadSheet.TAG)
+ }
+
+ R.id.action_playstore -> {
+ view.context.browse("${Constants.SHARE_URL}${app.packageName}")
+ }
+ }
+ true
+ }
+
+ if (::app.isInitialized) {
+ val installed = PackageUtil.isInstalled(requireContext(), app.packageName)
+ menu?.findItem(R.id.action_uninstall)?.isVisible = installed
+ uninstallActionEnabled = installed
+ }
+ }
+
+
+ activity?.onBackPressedDispatcher?.addCallback(this) {
+ if (isExternal) {
+ activity?.finish()
+ } else {
+ findNavController().navigateUp()
+ }
+ }
+ }
+
+ override fun onResume() {
+ getUpdateServiceInstance()
+ checkAndSetupInstall()
+ super.onResume()
+ }
+
+ private fun onNewIntent(intent: Intent) {
+ if (intent.scheme != null && (intent.scheme == "market" || intent.scheme == "http" || intent.scheme == "https")) {
+ val packageName = intent.data!!.getQueryParameter("id")
+ val packageVersion = intent.data!!.getQueryParameter("v")
+ if (packageName.isNullOrEmpty()) {
+ activity?.finishAfterTransition()
+ } else {
+ isExternal = true
+ app = App(packageName)
+ if (!packageVersion.isNullOrEmpty()) {
+ app.versionCode = packageVersion.toInt()
+ }
+ fetchCompleteApp()
+ }
+ } else {
+ val rawApp: String? = intent.getStringExtra(Constants.STRING_EXTRA)
+ if (rawApp != null) {
+ app = gson.fromJson(rawApp, App::class.java)
+ isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
+
+ inflatePartialApp()
+ fetchCompleteApp()
+ } else {
+ activity?.finishAfterTransition()
+ }
+ }
+ }
+
+ private fun attachActions() {
+ flip(0)
+ checkAndSetupInstall()
+ }
+
+ private fun updateActionState(state: State) {
+ binding.layoutDetailsInstall.btnDownload.updateState(state)
+ }
+
+ private fun openApp() {
+ val intent = PackageUtil.getLaunchIntent(requireContext(), app.packageName)
+ if (intent != null) {
+ try {
+ startActivity(intent)
+ } catch (e: ActivityNotFoundException) {
+ toast("Unable to open app")
+ }
+ }
+ }
+
+ private fun verifyAndInstall(files: List) {
+ if (downloadOnly)
+ return
+
+ var filesExist = true
+
+ files.forEach { download ->
+ filesExist = filesExist && File(download.file).exists()
+ }
+
+ if (filesExist)
+ install(files)
+ else
+ purchase()
+ }
+
+ @Synchronized
+ private fun install(files: List) {
+ updateActionState(State.IDLE)
+
+ val apkFiles = files.filter { it.file.endsWith(".apk") }
+ val preferredInstaller =
+ Preferences.getInteger(requireContext(), Preferences.PREFERENCE_INSTALLER_ID)
+
+ if (apkFiles.size > 1 && preferredInstaller == 1) {
+ showDialog(R.string.title_installer, R.string.dialog_desc_native_split)
+ } else {
+ task {
+ AppInstaller.getInstance(requireContext())
+ .getPreferredInstaller()
+ .install(
+ app.packageName,
+ apkFiles.map { it.file }
+ )
+ } fail {
+ Log.e(it.stackTraceToString())
+ }
+
+ runOnUiThread {
+ binding.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing))
+ }
+ }
+ }
+
+ @Synchronized
+ private fun uninstallApp() {
+ task {
+ AppInstaller.getInstance(requireContext())
+ .getPreferredInstaller()
+ .uninstall(app.packageName)
+ }
+ }
+
+ private fun attachWhiteListStatus() {
+
+ }
+
+ private fun fetchCompleteApp() {
+ task {
+ authData = AuthProvider.with(requireContext()).getAuthData()
+ return@task AppDetailsHelper(authData)
+ .using(HttpClient.getPreferredClient())
+ .getAppByPackageName(app.packageName)
+ } successUi {
+ if (isExternal) {
+ app = it
+ inflatePartialApp()
+ }
+ inflateExtraDetails(it)
+ } failUi {
+ toast("Failed to fetch app details")
+ }
+ }
+
+ private fun inflatePartialApp() {
+ if (::app.isInitialized) {
+ attachWhiteListStatus()
+ attachHeader()
+ attachBottomSheet()
+ attachFetch()
+ attachActions()
+
+ if (autoDownload) {
+ purchase()
+ }
+ }
+ }
+
+ private fun attachHeader() {
+ binding.layoutDetailsApp.apply {
+ imgIcon.load(app.iconArtwork.url) {
+ placeholder(R.drawable.bg_placeholder)
+ transform(RoundedCorners(32))
+ }
+
+ txtLine1.text = app.displayName
+ txtLine2.text = app.developerName
+ txtLine2.setOnClickListener {
+ NavigationUtil.openDevAppsActivity(
+ requireContext(),
+ app
+ )
+ }
+ txtLine3.text = ("${app.versionName} (${app.versionCode})")
+ packageName.text = app.packageName
+
+ val tags = mutableListOf()
+ if (app.isFree)
+ tags.add(getString(R.string.details_free))
+ else
+ tags.add(getString(R.string.details_paid))
+
+ if (app.containsAds)
+ tags.add(getString(R.string.details_contains_ads))
+ else
+ tags.add(getString(R.string.details_no_ads))
+
+ txtLine4.text = tags.joinToString(separator = " • ")
+ }
+ }
+
+ private fun inflateExtraDetails(app: App?) {
+ app?.let {
+ binding.viewFlipper.displayedChild = 1
+ inflateAppDescription(binding.layoutDetailDescription, app)
+ inflateAppRatingAndReviews(binding.layoutDetailsReview, app)
+ inflateAppDevInfo(binding.layoutDetailsDev, app)
+ inflateAppPrivacy(binding.layoutDetailsPrivacy, app)
+ inflateAppPermission(binding.layoutDetailsPermissions, app)
+
+ if (!authData.isAnonymous) {
+ app.testingProgram?.let {
+ if (it.isAvailable && it.isSubscribed) {
+ binding.layoutDetailsApp.txtLine1.text = it.displayName
+ }
+ }
+
+ inflateBetaSubscription(binding.layoutDetailsBeta, app)
+ }
+
+ if (Preferences.getBoolean(requireContext(), Preferences.PREFERENCE_SIMILAR)) {
+ inflateAppStream(binding.epoxyRecyclerStream, app)
+ }
+ }
+ }
+
+ @Synchronized
+ private fun startDownload() {
+ when (status) {
+ Status.PAUSED -> {
+ fetch?.resumeGroup(app.getGroupId(requireContext()))
+ }
+
+ Status.DOWNLOADING -> {
+ flip(1)
+ toast("Already downloading")
+ }
+
+ Status.COMPLETED -> {
+ fetch?.getFetchGroup(app.getGroupId(requireContext())) {
+ verifyAndInstall(it.downloads)
+ }
+ }
+
+ else -> {
+ purchase()
+ }
+ }
+ }
+
+ private fun purchase() {
+ bottomSheetBehavior.isHideable = false
+ bottomSheetBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
+ updateActionState(State.PROGRESS)
+
+ if (PathUtil.needsStorageManagerPerm(app.fileList) || requireContext().isExternalStorageEnable()) {
+ if (isRAndAbove()) {
+ if (!Environment.isExternalStorageManager()) {
+ startForStorageManagerResult.launch(
+ Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
+ )
+ } else {
+ updateApp(app)
+ }
+ } else {
+ if (ContextCompat.checkSelfPermission(
+ requireContext(),
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ ) == PackageManager.PERMISSION_GRANTED
+ ) {
+ updateApp(app)
+ } else {
+ startForPermissions.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ }
+ }
+ } else {
+ updateApp(app)
+ }
+ }
+
+ private fun updateApp(app: App) {
+ if (updateService == null) {
+ listOfActionsWhenServiceAttaches.add {
+ updateService?.updateApp(app, true)
+ }
+ getUpdateServiceInstance()
+ } else {
+ updateService?.updateApp(app, true)
+ }
+ }
+
+ private fun updateProgress(
+ fetchGroup: FetchGroup,
+ etaInMilliSeconds: Long,
+ downloadedBytesPerSecond: Long
+ ) {
+ runOnUiThread {
+ val progress = if (fetchGroup.groupDownloadProgress > 0)
+ fetchGroup.groupDownloadProgress
+ else
+ 0
+
+ if (progress == 100) {
+ binding.layoutDetailsInstall.btnDownload.setText(getString(R.string.action_installing))
+ return@runOnUiThread
+ }
+ binding.layoutDetailsInstall.apply {
+ txtProgressPercent.text = ("${progress}%")
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ progressDownload.setProgress(progress, true)
+ } else {
+ progressDownload.progress = progress
+ }
+
+ txtEta.text = CommonUtil.getETAString(
+ requireContext(),
+ etaInMilliSeconds
+ )
+ txtSpeed.text =
+ CommonUtil.getDownloadSpeedString(
+ requireContext(),
+ downloadedBytesPerSecond
+ )
+ }
+ }
+ }
+
+ private fun expandBottomSheet(message: String?) {
+ bottomSheetBehavior.isHideable = false
+ bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
+
+ with(binding.layoutDetailsInstall) {
+ txtPurchaseError.text = message
+ btnDownload.updateState(State.IDLE)
+ if (app.isFree)
+ btnDownload.setText(R.string.action_install)
+ else
+ btnDownload.setText(app.price)
+ }
+ }
+
+ private fun checkAndSetupInstall() {
+ isInstalled = PackageUtil.isInstalled(requireContext(), app.packageName)
+
+ binding.layoutDetailsInstall.btnDownload.let { btn ->
+ if (isInstalled) {
+ isUpdatable = PackageUtil.isUpdatable(
+ requireContext(),
+ app.packageName,
+ app.versionCode.toLong()
+ )
+
+ val installedVersion =
+ PackageUtil.getInstalledVersion(requireContext(), app.packageName)
+
+ if (isUpdatable) {
+ binding.layoutDetailsApp.txtLine3.text =
+ ("$installedVersion ➔ ${app.versionName} (${app.versionCode})")
+ btn.setText(R.string.action_update)
+ btn.addOnClickListener { startDownload() }
+ } else {
+ binding.layoutDetailsApp.txtLine3.text = installedVersion
+ btn.setText(R.string.action_open)
+ btn.addOnClickListener { openApp() }
+ }
+ if (!uninstallActionEnabled) {
+ binding.layoutDetailsToolbar.toolbar.invalidateMenu()
+ }
+ } else {
+ if (app.isFree) {
+ btn.setText(R.string.action_install)
+ } else {
+ btn.setText(app.price)
+ }
+
+ btn.addOnClickListener {
+ if (authData.isAnonymous && !app.isFree) {
+ toast(R.string.toast_purchase_blocked)
+ } else {
+ btn.setText(R.string.download_metadata)
+ startDownload()
+ }
+ }
+ if (uninstallActionEnabled) {
+ binding.layoutDetailsToolbar.toolbar.invalidateMenu()
+ }
+ }
+ }
+ }
+
+ @Synchronized
+ private fun flip(nextView: Int) {
+ runOnUiThread {
+ val displayChild = binding.layoutDetailsInstall.viewFlipper.displayedChild
+ if (displayChild != nextView) {
+ binding.layoutDetailsInstall.viewFlipper.displayedChild = nextView
+ if (nextView == 0) checkAndSetupInstall()
+ }
+ }
+ }
+
+ private fun attachFetch() {
+ if (fetch == null) {
+ downloadManager = DownloadManager.with(requireContext())
+ fetch = downloadManager!!.fetch
+ }
+ fetch?.getFetchGroup(app.getGroupId(requireContext())) { fetchGroup: FetchGroup ->
+ if (fetchGroup.groupDownloadProgress == 100 && fetchGroup.completedDownloads.isNotEmpty()) {
+ status = Status.COMPLETED
+ } else if (downloadManager?.isDownloading(fetchGroup) == true) {
+ status = Status.DOWNLOADING
+ flip(1)
+ } else if (downloadManager?.isCanceled(fetchGroup) == true) {
+ status = Status.CANCELLED
+ } else if (fetchGroup.pausedDownloads.isNotEmpty()) {
+ status = Status.PAUSED
+ } else {
+ status = Status.NONE
+ }
+ }
+
+ fetchGroupListener = object : AbstractFetchGroupListener() {
+
+ override fun onAdded(groupId: Int, download: Download, fetchGroup: FetchGroup) {
+ if (groupId == app.getGroupId(requireContext())) {
+ status = download.status
+ }
+ }
+
+ override fun onStarted(
+ groupId: Int,
+ download: Download,
+ downloadBlocks: List,
+ totalBlocks: Int,
+ fetchGroup: FetchGroup
+ ) {
+ if (groupId == app.getGroupId(requireContext())) {
+ status = download.status
+ flip(1)
+
+ val pkgDir = PathUtil.getPackageDirectory(requireContext(), app.packageName)
+ completionMarker =
+ File("$pkgDir/.${app.versionCode}.download-complete")
+ inProgressMarker =
+ File("$pkgDir/.${app.versionCode}.download-in-progress")
+
+ if (completionMarker.exists())
+ completionMarker.delete()
+
+ inProgressMarker.createNewFile()
+ }
+ }
+
+ override fun onResumed(groupId: Int, download: Download, fetchGroup: FetchGroup) {
+ if (groupId == app.getGroupId(requireContext())) {
+ status = download.status
+ flip(1)
+ inProgressMarker.parentFile?.mkdirs()
+ inProgressMarker.createNewFile()
+ }
+ }
+
+ override fun onPaused(groupId: Int, download: Download, fetchGroup: FetchGroup) {
+ if (groupId == app.getGroupId(requireContext())) {
+ status = download.status
+ flip(0)
+ }
+ }
+
+ override fun onProgress(
+ groupId: Int,
+ download: Download,
+ etaInMilliSeconds: Long,
+ downloadedBytesPerSecond: Long,
+ fetchGroup: FetchGroup
+ ) {
+ if (groupId == app.getGroupId(requireContext())) {
+ updateProgress(fetchGroup, etaInMilliSeconds, downloadedBytesPerSecond)
+ Log.i(
+ "${app.displayName} : ${download.file} -> Progress : %d",
+ fetchGroup.groupDownloadProgress
+ )
+ }
+ }
+
+ override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) {
+ if (groupId == app.getGroupId(requireContext()) && fetchGroup.groupDownloadProgress == 100) {
+ status = download.status
+ flip(0)
+ updateProgress(fetchGroup, -1, -1)
+ try {
+ inProgressMarker.delete()
+ completionMarker.createNewFile()
+ } catch (ex: Exception) {
+ ex.printStackTrace()
+ }
+ }
+ }
+
+ override fun onCancelled(groupId: Int, download: Download, fetchGroup: FetchGroup) {
+ if (groupId == app.getGroupId(requireContext())) {
+ status = download.status
+ flip(0)
+ inProgressMarker.delete()
+ }
+ }
+
+ override fun onError(
+ groupId: Int,
+ download: Download,
+ error: Error,
+ throwable: Throwable?,
+ fetchGroup: FetchGroup
+ ) {
+ if (groupId == app.getGroupId(requireContext())) {
+ status = download.status
+ flip(0)
+ inProgressMarker.delete()
+ }
+ }
+ }
+
+ appMetadataListener = object : AppMetadataStatusListener {
+ override fun onAppMetadataStatusError(reason: String, app: App) {
+ if (app.packageName == this@AppDetailsFragment.app.packageName) {
+ updateActionState(State.IDLE)
+ expandBottomSheet(reason)
+ }
+ }
+ }
+
+ getUpdateServiceInstance()
+
+ binding.layoutDetailsInstall.imgCancel.setOnClickListener {
+ fetch?.cancelGroup(
+ app.getGroupId(requireContext())
+ )
+ }
+ if (updateService != null) {
+ pendingAddListeners = false
+ updateService!!.registerFetchListener(fetchGroupListener)
+ // appMetadataListener needs to be initialized after the fetchGroupListener
+ updateService!!.registerAppMetadataListener(appMetadataListener)
+ } else {
+ pendingAddListeners = true
+ }
+ }
+
+ private fun getUpdateServiceInstance() {
+ if (updateService == null && !attachToServiceCalled) {
+ attachToServiceCalled = true
+ val intent = Intent(requireContext(), UpdateService::class.java)
+ activity?.startService(intent)
+ activity?.bindService(
+ intent,
+ serviceConnection,
+ 0
+ )
+ }
+ }
+
+ override fun onPause() {
+ if (updateService != null) {
+ updateService = null
+ attachToServiceCalled = false
+ pendingAddListeners = true
+ activity?.unbindService(serviceConnection)
+ }
+ super.onPause()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ if (updateService != null) {
+ updateService = null
+ attachToServiceCalled = false
+ pendingAddListeners = true
+ activity?.unbindService(serviceConnection)
+ }
+ }
+
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
+ private fun attachBottomSheet() {
+ binding.layoutDetailsInstall.apply {
+ viewFlipper.setInAnimation(requireContext(), R.anim.fade_in)
+ viewFlipper.setOutAnimation(requireContext(), R.anim.fade_out)
+ }
+
+ bottomSheetBehavior = BottomSheetBehavior.from(binding.layoutDetailsInstall.bottomSheet)
+ bottomSheetBehavior.isDraggable = false
+
+ bottomSheetBehavior.addBottomSheetCallback(object : BottomSheetCallback() {
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
+ if (newState == BottomSheetBehavior.STATE_EXPANDED) {
+ bottomSheetBehavior.setDraggable(true)
+ } else if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
+ bottomSheetBehavior.isDraggable = false
+ }
+ }
+
+ override fun onSlide(bottomSheet: View, slideOffset: Float) {}
+ })
+ }
+
+ //Sub Section Inflation
+ private fun inflateAppDescription(B: LayoutDetailsDescriptionBinding, app: App) {
+ val installs = CommonUtil.addDiPrefix(app.installs)
+
+ if (installs != "NA") {
+ B.txtInstalls.text = CommonUtil.addDiPrefix(app.installs)
+ } else {
+ B.txtInstalls.hide()
+ }
+
+ B.txtSize.text = CommonUtil.addSiPrefix(app.size)
+ B.txtRating.text = app.labeledRating
+ B.txtSdk.text = ("Target SDK ${app.targetSdk}")
+ B.txtUpdated.text = app.updatedOn
+ B.txtDescription.text = HtmlCompat.fromHtml(
+ app.shortDescription,
+ HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS
+ )
+
+ app.changes.apply {
+ if (isEmpty()) {
+ B.txtChangelog.text = getString(R.string.details_changelog_unavailable)
+ } else {
+ B.txtChangelog.text = HtmlCompat.fromHtml(
+ this,
+ HtmlCompat.FROM_HTML_MODE_COMPACT
+ )
+ }
+ }
+
+ B.headerDescription.addClickListener {
+ (activity as MainActivity).openDetailsMoreActivity(app)
+ }
+
+ B.epoxyRecycler.withModels {
+ setFilterDuplicates(true)
+ var position = 0
+ app.screenshots
+ //.sortedWith { o1, o2 -> o2.height.compareTo(o1.height) }
+ .forEach { artwork ->
+ add(
+ ScreenshotViewModel_()
+ .id(artwork.url)
+ .artwork(artwork)
+ .position(position++)
+ .callback(object : ScreenshotView.ScreenshotCallback {
+ override fun onClick(position: Int) {
+ (activity as MainActivity).openScreenshotActivity(app, position)
+ }
+ })
+ )
+ }
+ }
+ }
+
+ private fun inflateAppRatingAndReviews(B: LayoutDetailsReviewBinding, app: App) {
+ B.averageRating.text = app.rating.average.toString()
+ B.txtReviewCount.text = app.rating.abbreviatedLabel
+
+ var totalStars = 0L
+ totalStars += app.rating.oneStar
+ totalStars += app.rating.twoStar
+ totalStars += app.rating.threeStar
+ totalStars += app.rating.fourStar
+ totalStars += app.rating.fiveStar
+
+ B.avgRatingLayout.apply {
+ removeAllViews()
+ addView(addAvgReviews(5, totalStars, app.rating.fiveStar))
+ addView(addAvgReviews(4, totalStars, app.rating.fourStar))
+ addView(addAvgReviews(3, totalStars, app.rating.threeStar))
+ addView(addAvgReviews(2, totalStars, app.rating.twoStar))
+ addView(addAvgReviews(1, totalStars, app.rating.oneStar))
+ }
+
+ B.averageRating.text = String.format(Locale.getDefault(), "%.1f", app.rating.average)
+ B.txtReviewCount.text = app.rating.abbreviatedLabel
+
+ val authData = AuthProvider.with(requireContext()).getAuthData()
+
+ B.layoutUserReview.visibility = if (authData.isAnonymous) View.GONE else View.VISIBLE
+
+ B.btnPostReview.setOnClickListener {
+ if (authData.isAnonymous) {
+ toast(R.string.toast_anonymous_restriction)
+ } else {
+ addOrUpdateReview(B, app, Review().apply {
+ title = authData.userProfile!!.name
+ rating = B.userStars.rating.toInt()
+ comment = B.inputReview.text.toString()
+ })
+ }
+ }
+
+ B.headerRatingReviews.addClickListener {
+ (activity as MainActivity).openDetailsReviewActivity(app)
+ }
+
+ task {
+ fetchReviewSummary(app)
+ } successUi {
+ B.epoxyRecycler.withModels {
+ it.take(4)
+ .forEach {
+ add(
+ ReviewViewModel_()
+ .id(it.timeStamp)
+ .review(it)
+ )
+ }
+ }
+ } failUi {
+
+ }
+
+ }
+
+ private fun inflateAppPrivacy(B: LayoutDetailsPrivacyBinding, app: App) {
+
+ task {
+ fetchReport(app.packageName)
+ } successUi { report ->
+ if (report.trackers.isNotEmpty()) {
+ B.txtStatus.apply {
+ setTextColor(
+ ContextCompat.getColor(
+ requireContext(),
+ if (report.trackers.size > 4)
+ R.color.colorRed
+ else
+ R.color.colorOrange
+ )
+ )
+ text =
+ ("${report.trackers.size} ${getString(R.string.exodus_substring)} ${report.version}")
+ }
+
+ B.headerPrivacy.addClickListener {
+ NavigationUtil.openExodusActivity(requireContext(), app, report)
+ }
+ } else {
+ B.txtStatus.apply {
+ setTextColor(
+ ContextCompat.getColor(
+ requireContext(),
+ R.color.colorGreen
+ )
+ )
+ text = getString(R.string.exodus_no_tracker)
+ }
+ }
+ } failUi {
+ B.txtStatus.text = it.message
+ }
+ }
+
+ private fun inflateAppDevInfo(B: LayoutDetailsDevBinding, app: App) {
+ if (app.developerAddress.isNotEmpty()) {
+ B.devAddress.apply {
+ setTxtSubtitle(
+ HtmlCompat.fromHtml(
+ app.developerAddress,
+ HtmlCompat.FROM_HTML_MODE_LEGACY
+ ).toString()
+ )
+ visibility = View.VISIBLE
+ }
+ }
+
+ if (app.developerWebsite.isNotEmpty()) {
+ B.devWeb.apply {
+ setTxtSubtitle(app.developerWebsite)
+ visibility = View.VISIBLE
+ }
+ }
+
+ if (app.developerEmail.isNotEmpty()) {
+ B.devMail.apply {
+ setTxtSubtitle(app.developerEmail)
+ visibility = View.VISIBLE
+ }
+ }
+ }
+
+ private fun inflateBetaSubscription(B: LayoutDetailsBetaBinding, app: App) {
+ app.testingProgram?.let { betaProgram ->
+ if (betaProgram.isAvailable) {
+ B.root.show()
+
+ updateBetaActions(B, betaProgram.isSubscribed)
+
+ if (betaProgram.isSubscribedAndInstalled) {
+
+ }
+
+ B.imgBeta.load(betaProgram.artwork.url) {
+
+ }
+
+ B.btnBetaAction.setOnClickListener {
+ val authData = AuthProvider.with(requireContext()).getAuthData()
+ task {
+ B.btnBetaAction.text = getString(R.string.action_pending)
+ B.btnBetaAction.isEnabled = false
+ AppDetailsHelper(authData).testingProgram(
+ app.packageName,
+ !betaProgram.isSubscribed
+ )
+ } successUi {
+ B.btnBetaAction.isEnabled = true
+ if (it.subscribed) {
+ updateBetaActions(B, true)
+ }
+ if (it.unsubscribed) {
+ updateBetaActions(B, false)
+ }
+ } failUi {
+ updateBetaActions(B, betaProgram.isSubscribed)
+ toast(getString(R.string.details_beta_delay))
+ }
+ }
+ } else {
+ B.root.hide()
+ }
+ }
+ }
+
+ private fun inflateAppStream(epoxyRecyclerView: EpoxyRecyclerView, app: App) {
+ app.detailsStreamUrl?.let {
+ val VM = ViewModelProvider(this)[DetailsClusterViewModel::class.java]
+
+ val carouselController =
+ DetailsCarouselController(object : GenericCarouselController.Callbacks {
+ override fun onHeaderClicked(streamCluster: StreamCluster) {
+ if (streamCluster.clusterBrowseUrl.isNotEmpty())
+ openStreamBrowseActivity(
+ streamCluster.clusterBrowseUrl,
+ streamCluster.clusterTitle
+ )
+ else
+ toast(getString(R.string.toast_page_unavailable))
+ }
+
+ override fun onClusterScrolled(streamCluster: StreamCluster) {
+ VM.observeCluster(streamCluster)
+ }
+
+ override fun onAppClick(app: App) {
+ openDetailsActivity(app)
+ }
+
+ override fun onAppLongClick(app: App) {
+
+ }
+ })
+
+ VM.liveData.observe(viewLifecycleOwner) {
+ when (it) {
+ is ViewState.Empty -> {
+ }
+
+ is ViewState.Loading -> {
+
+ }
+
+ is ViewState.Error -> {
+
+ }
+
+ is ViewState.Status -> {
+
+ }
+
+ is ViewState.Success<*> -> {
+ carouselController.setData(it.data as StreamBundle)
+ }
+ }
+ }
+
+ epoxyRecyclerView.setController(carouselController)
+
+ VM.getStreamBundle(it)
+ }
+ }
+
+ private fun inflateAppPermission(B: LayoutDetailsPermissionsBinding, app: App) {
+ B.headerPermission.addClickListener {
+ if (app.permissions.size > 0) {
+ PermissionBottomSheet.newInstance(app)
+ .show(childFragmentManager, PermissionBottomSheet.TAG)
+ }
+ }
+ B.txtPermissionCount.text = ("${app.permissions.size} permissions")
+ }
+
+ private fun updateBetaActions(B: LayoutDetailsBetaBinding, isSubscribed: Boolean) {
+ if (isSubscribed) {
+ B.btnBetaAction.text = getString(R.string.action_leave)
+ B.txtBetaTitle.text = getString(R.string.details_beta_subscribed)
+ } else {
+ B.btnBetaAction.text = getString(R.string.action_join)
+ B.txtBetaTitle.text = getString(R.string.details_beta_available)
+ }
+ }
+
+ /* App Review Helpers */
+
+ private fun addAvgReviews(number: Int, max: Long, rating: Long): RelativeLayout {
+ return RatingView(requireContext(), number, max.toInt(), rating.toInt())
+ }
+
+ private fun addOrUpdateReview(
+ B: LayoutDetailsReviewBinding,
+ app: App,
+ review: Review,
+ isBeta: Boolean = false
+ ) {
+ task {
+ val authData = AuthProvider.with(requireContext()).getAuthData()
+ ReviewsHelper(authData)
+ .using(HttpClient.getPreferredClient())
+ .addOrEditReview(
+ app.packageName,
+ review.title,
+ review.comment,
+ review.rating,
+ isBeta
+ )
+ }.successUi {
+ it?.let {
+ B.userStars.rating = it.rating.toFloat()
+ Toast.makeText(
+ requireContext(),
+ getString(R.string.toast_rated_success),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }.failUi {
+ Toast.makeText(
+ requireContext(),
+ getString(R.string.toast_rated_failed),
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+
+ private fun fetchReviewSummary(app: App): List {
+ val authData = AuthProvider
+ .with(requireContext())
+ .getAuthData()
+ val reviewsHelper = ReviewsHelper(authData)
+ .using(HttpClient.getPreferredClient())
+ return reviewsHelper.getReviewSummary(app.packageName)
+ }
+
+ /* App Privacy Helpers */
+
+ private fun parseResponse(response: String, packageName: String): List {
+ try {
+ val jsonObject = JSONObject(response)
+ val exodusObject = jsonObject.getJSONObject(packageName)
+ val exodusReport: ExodusReport = gson.fromJson(
+ exodusObject.toString(),
+ ExodusReport::class.java
+ )
+ return exodusReport.reports
+ } catch (e: Exception) {
+ throw Exception("No reports found")
+ }
+ }
+
+ private fun fetchReport(packageName: String): Report {
+ val headers: MutableMap = mutableMapOf()
+ headers["Content-Type"] = "application/json"
+ headers["Accept"] = "application/json"
+ headers["Authorization"] = exodusApiKey
+
+ val url = exodusBaseUrl + packageName
+
+ val playResponse = HttpClient
+ .getPreferredClient()
+ .get(url, headers)
+
+ if (playResponse.isSuccessful) {
+ return parseResponse(String(playResponse.responseBytes), packageName)[0]
+ } else {
+ throw Exception("Failed to fetch report")
+ }
+ }
+}
diff --git a/app/src/main/java/com/aurora/store/view/ui/details/BaseDetailsActivity.kt b/app/src/main/java/com/aurora/store/view/ui/details/BaseDetailsActivity.kt
deleted file mode 100644
index 76a9920b3..000000000
--- a/app/src/main/java/com/aurora/store/view/ui/details/BaseDetailsActivity.kt
+++ /dev/null
@@ -1,443 +0,0 @@
-/*
- * Aurora Store
- * Copyright (C) 2021, Rahul Kumar Patel
- *
- * Aurora Store 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 2 of the License, or
- * (at your option) any later version.
- *
- * Aurora Store 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 Aurora Store. If not, see .
- *
- */
-
-package com.aurora.store.view.ui.details
-
-import android.view.View
-import android.widget.RelativeLayout
-import android.widget.Toast
-import androidx.core.content.ContextCompat
-import androidx.core.text.HtmlCompat
-import androidx.lifecycle.ViewModelProvider
-import com.airbnb.epoxy.EpoxyRecyclerView
-import com.aurora.extensions.hide
-import com.aurora.extensions.load
-import com.aurora.extensions.show
-import com.aurora.extensions.toast
-import com.aurora.gplayapi.data.models.*
-import com.aurora.gplayapi.helpers.AppDetailsHelper
-import com.aurora.gplayapi.helpers.ReviewsHelper
-import com.aurora.store.R
-import com.aurora.store.data.ViewState
-import com.aurora.store.data.model.ExodusReport
-import com.aurora.store.data.model.Report
-import com.aurora.store.data.network.HttpClient
-import com.aurora.store.data.providers.AuthProvider
-import com.aurora.store.databinding.*
-import com.aurora.store.util.CommonUtil
-import com.aurora.store.util.NavigationUtil
-import com.aurora.store.view.custom.RatingView
-import com.aurora.store.view.epoxy.controller.DetailsCarouselController
-import com.aurora.store.view.epoxy.controller.GenericCarouselController
-import com.aurora.store.view.epoxy.views.details.ReviewViewModel_
-import com.aurora.store.view.epoxy.views.details.ScreenshotView
-import com.aurora.store.view.epoxy.views.details.ScreenshotViewModel_
-import com.aurora.store.view.ui.commons.BaseActivity
-import com.aurora.store.view.ui.sheets.PermissionBottomSheet
-import com.aurora.store.viewmodel.details.DetailsClusterViewModel
-import nl.komponents.kovenant.task
-import nl.komponents.kovenant.ui.failUi
-import nl.komponents.kovenant.ui.successUi
-import org.json.JSONObject
-import java.util.*
-
-
-abstract class BaseDetailsActivity : BaseActivity() {
-
- private val exodusBaseUrl = "https://reports.exodus-privacy.eu.org/api/search/"
- private val exodusApiKey = "Token bbe6ebae4ad45a9cbacb17d69739799b8df2c7ae"
-
- //Sub Section Inflation
- fun inflateAppDescription(B: LayoutDetailsDescriptionBinding, app: App) {
- val installs = CommonUtil.addDiPrefix(app.installs)
-
- if (installs != "NA") {
- B.txtInstalls.text = CommonUtil.addDiPrefix(app.installs)
- } else {
- B.txtInstalls.hide()
- }
-
- B.txtSize.text = CommonUtil.addSiPrefix(app.size)
- B.txtRating.text = app.labeledRating
- B.txtSdk.text = ("Target SDK ${app.targetSdk}")
- B.txtUpdated.text = app.updatedOn
- B.txtDescription.text = HtmlCompat.fromHtml(
- app.shortDescription,
- HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS
- )
-
- app.changes.apply {
- if (isEmpty()) {
- B.txtChangelog.text = getString(R.string.details_changelog_unavailable)
- } else {
- B.txtChangelog.text = HtmlCompat.fromHtml(
- this,
- HtmlCompat.FROM_HTML_MODE_COMPACT
- )
- }
- }
-
- B.headerDescription.addClickListener {
- openDetailsMoreActivity(app)
- }
-
- B.epoxyRecycler.withModels {
- setFilterDuplicates(true)
- var position = 0
- app.screenshots
- //.sortedWith { o1, o2 -> o2.height.compareTo(o1.height) }
- .forEach { artwork ->
- add(
- ScreenshotViewModel_()
- .id(artwork.url)
- .artwork(artwork)
- .position(position++)
- .callback(object : ScreenshotView.ScreenshotCallback {
- override fun onClick(position: Int) {
- openScreenshotActivity(app, position)
- }
- })
- )
- }
- }
- }
-
- fun inflateAppRatingAndReviews(B: LayoutDetailsReviewBinding, app: App) {
- B.averageRating.text = app.rating.average.toString()
- B.txtReviewCount.text = app.rating.abbreviatedLabel
-
- var totalStars = 0L
- totalStars += app.rating.oneStar
- totalStars += app.rating.twoStar
- totalStars += app.rating.threeStar
- totalStars += app.rating.fourStar
- totalStars += app.rating.fiveStar
-
- B.avgRatingLayout.apply {
- removeAllViews()
- addView(addAvgReviews(5, totalStars, app.rating.fiveStar))
- addView(addAvgReviews(4, totalStars, app.rating.fourStar))
- addView(addAvgReviews(3, totalStars, app.rating.threeStar))
- addView(addAvgReviews(2, totalStars, app.rating.twoStar))
- addView(addAvgReviews(1, totalStars, app.rating.oneStar))
- }
-
- B.averageRating.text = String.format(Locale.getDefault(), "%.1f", app.rating.average)
- B.txtReviewCount.text = app.rating.abbreviatedLabel
-
- val authData = AuthProvider.with(this).getAuthData()
-
- B.layoutUserReview.visibility = if (authData.isAnonymous) View.GONE else View.VISIBLE
-
- B.btnPostReview.setOnClickListener {
- if (authData.isAnonymous) {
- toast(R.string.toast_anonymous_restriction)
- } else {
- addOrUpdateReview(B, app, Review().apply {
- title = authData.userProfile!!.name
- rating = B.userStars.rating.toInt()
- comment = B.inputReview.text.toString()
- })
- }
- }
-
- B.headerRatingReviews.addClickListener {
- openDetailsReviewActivity(app)
- }
-
- task {
- fetchReviewSummary(app)
- } successUi {
- B.epoxyRecycler.withModels {
- it.take(4)
- .forEach {
- add(
- ReviewViewModel_()
- .id(it.timeStamp)
- .review(it)
- )
- }
- }
- } failUi {
-
- }
-
- }
-
- fun inflateAppPrivacy(B: LayoutDetailsPrivacyBinding, app: App) {
-
- task {
- fetchReport(app.packageName)
- } successUi { report ->
- if (report.trackers.isNotEmpty()) {
- B.txtStatus.apply {
- setTextColor(
- ContextCompat.getColor(
- this@BaseDetailsActivity,
- if (report.trackers.size > 4)
- R.color.colorRed
- else
- R.color.colorOrange
- )
- )
- text =
- ("${report.trackers.size} ${getString(R.string.exodus_substring)} ${report.version}")
- }
-
- B.headerPrivacy.addClickListener {
- NavigationUtil.openExodusActivity(this, app, report)
- }
- } else {
- B.txtStatus.apply {
- setTextColor(
- ContextCompat.getColor(
- this@BaseDetailsActivity,
- R.color.colorGreen
- )
- )
- text = getString(R.string.exodus_no_tracker)
- }
- }
- } failUi {
- B.txtStatus.text = it.message
- }
- }
-
- fun inflateAppDevInfo(B: LayoutDetailsDevBinding, app: App) {
- if (app.developerAddress.isNotEmpty()) {
- B.devAddress.apply {
- setTxtSubtitle(
- HtmlCompat.fromHtml(
- app.developerAddress,
- HtmlCompat.FROM_HTML_MODE_LEGACY
- ).toString()
- )
- visibility = View.VISIBLE
- }
- }
-
- if (app.developerWebsite.isNotEmpty()) {
- B.devWeb.apply {
- setTxtSubtitle(app.developerWebsite)
- visibility = View.VISIBLE
- }
- }
-
- if (app.developerEmail.isNotEmpty()) {
- B.devMail.apply {
- setTxtSubtitle(app.developerEmail)
- visibility = View.VISIBLE
- }
- }
- }
-
- fun inflateBetaSubscription(B: LayoutDetailsBetaBinding, app: App) {
- app.testingProgram?.let { betaProgram ->
- if (betaProgram.isAvailable) {
- B.root.show()
-
- updateBetaActions(B, betaProgram.isSubscribed)
-
- if (betaProgram.isSubscribedAndInstalled) {
-
- }
-
- B.imgBeta.load(betaProgram.artwork.url) {
-
- }
-
- B.btnBetaAction.setOnClickListener {
- val authData = AuthProvider.with(this).getAuthData()
- task {
- B.btnBetaAction.text = getString(R.string.action_pending)
- B.btnBetaAction.isEnabled = false
- AppDetailsHelper(authData).testingProgram(
- app.packageName,
- !betaProgram.isSubscribed
- )
- } successUi {
- B.btnBetaAction.isEnabled = true
- if (it.subscribed) {
- updateBetaActions(B, true)
- }
- if (it.unsubscribed) {
- updateBetaActions(B, false)
- }
- } failUi {
- updateBetaActions(B, betaProgram.isSubscribed)
- toast(getString(R.string.details_beta_delay))
- }
- }
- } else {
- B.root.hide()
- }
- }
- }
-
- fun inflateAppStream(epoxyRecyclerView: EpoxyRecyclerView, app: App) {
- app.detailsStreamUrl?.let {
- val VM = ViewModelProvider(this)[DetailsClusterViewModel::class.java]
-
- val carouselController =
- DetailsCarouselController(object : GenericCarouselController.Callbacks {
- override fun onHeaderClicked(streamCluster: StreamCluster) {
- if (streamCluster.clusterBrowseUrl.isNotEmpty())
- openStreamBrowseActivity(
- streamCluster.clusterBrowseUrl,
- streamCluster.clusterTitle
- )
- else
- toast(getString(R.string.toast_page_unavailable))
- }
-
- override fun onClusterScrolled(streamCluster: StreamCluster) {
- VM.observeCluster(streamCluster)
- }
-
- override fun onAppClick(app: App) {
- openDetailsActivity(app)
- }
-
- override fun onAppLongClick(app: App) {
-
- }
- })
-
- VM.liveData.observe(this) {
- when (it) {
- is ViewState.Empty -> {
- }
- is ViewState.Loading -> {
-
- }
- is ViewState.Error -> {
-
- }
- is ViewState.Status -> {
-
- }
- is ViewState.Success<*> -> {
- carouselController.setData(it.data as StreamBundle)
- }
- }
- }
-
- epoxyRecyclerView.setController(carouselController)
-
- VM.getStreamBundle(it)
- }
- }
-
- fun inflateAppPermission(B: LayoutDetailsPermissionsBinding, app: App) {
- B.headerPermission.addClickListener {
- if (app.permissions.size > 0) {
- PermissionBottomSheet.newInstance(app)
- .show(supportFragmentManager, PermissionBottomSheet.TAG)
- }
- }
- B.txtPermissionCount.text = ("${app.permissions.size} permissions")
- }
-
- private fun updateBetaActions(B: LayoutDetailsBetaBinding, isSubscribed: Boolean) {
- if (isSubscribed) {
- B.btnBetaAction.text = getString(R.string.action_leave)
- B.txtBetaTitle.text = getString(R.string.details_beta_subscribed)
- } else {
- B.btnBetaAction.text = getString(R.string.action_join)
- B.txtBetaTitle.text = getString(R.string.details_beta_available)
- }
- }
-
- /* App Review Helpers */
-
- private fun addAvgReviews(number: Int, max: Long, rating: Long): RelativeLayout {
- return RatingView(this, number, max.toInt(), rating.toInt())
- }
-
- private fun addOrUpdateReview(
- B: LayoutDetailsReviewBinding,
- app: App,
- review: Review,
- isBeta: Boolean = false
- ) {
- task {
- val authData = AuthProvider.with(this).getAuthData()
- ReviewsHelper(authData)
- .using(HttpClient.getPreferredClient())
- .addOrEditReview(
- app.packageName,
- review.title,
- review.comment,
- review.rating,
- isBeta
- )
- }.successUi {
- it?.let {
- B.userStars.rating = it.rating.toFloat()
- Toast.makeText(this, getString(R.string.toast_rated_success), Toast.LENGTH_SHORT).show()
- }
- }.failUi {
- Toast.makeText(this, getString(R.string.toast_rated_failed), Toast.LENGTH_SHORT).show()
- }
- }
-
- private fun fetchReviewSummary(app: App): List {
- val authData = AuthProvider
- .with(this)
- .getAuthData()
- val reviewsHelper = ReviewsHelper(authData)
- .using(HttpClient.getPreferredClient())
- return reviewsHelper.getReviewSummary(app.packageName)
- }
-
- /* App Privacy Helpers */
-
- private fun parseResponse(response: String, packageName: String): List {
- try {
- val jsonObject = JSONObject(response)
- val exodusObject = jsonObject.getJSONObject(packageName)
- val exodusReport: ExodusReport = gson.fromJson(
- exodusObject.toString(),
- ExodusReport::class.java
- )
- return exodusReport.reports
- } catch (e: Exception) {
- throw Exception("No reports found")
- }
- }
-
- private fun fetchReport(packageName: String): Report {
- val headers: MutableMap = mutableMapOf()
- headers["Content-Type"] = "application/json"
- headers["Accept"] = "application/json"
- headers["Authorization"] = exodusApiKey
-
- val url = exodusBaseUrl + packageName
-
- val playResponse = HttpClient
- .getPreferredClient()
- .get(url, headers)
-
- if (playResponse.isSuccessful) {
- return parseResponse(String(playResponse.responseBytes), packageName)[0]
- } else {
- throw Exception("Failed to fetch report")
- }
- }
-}
diff --git a/app/src/main/java/com/aurora/store/view/ui/details/EmptyAppDetailsActivity.kt b/app/src/main/java/com/aurora/store/view/ui/details/EmptyAppDetailsActivity.kt
deleted file mode 100644
index c38d96af6..000000000
--- a/app/src/main/java/com/aurora/store/view/ui/details/EmptyAppDetailsActivity.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.aurora.store.view.ui.details
-
-import android.content.Intent
-import android.os.Bundle
-import androidx.appcompat.app.AppCompatActivity
-import com.aurora.store.R
-
-class EmptyAppDetailsActivity: AppCompatActivity(R.layout.activity_details) {
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- onNewIntent(intent)
- }
-
- override fun onNewIntent(intent: Intent?) {
- super.onNewIntent(intent)
- val validSchemes = listOf("market", "http", "https")
-
- if (intent != null && validSchemes.any { it == intent.scheme }) {
- if (intent.data!!.getQueryParameter("id").isNullOrEmpty()) {
- finishAfterTransition()
- } else {
- // Construct a new intent manually to avoid accepting extras from external apps
- Intent(this, AppDetailsActivity::class.java).also { extIntent ->
- extIntent.data = intent.data
- startActivity(extIntent)
- }
- }
-
- }
- }
-}
diff --git a/app/src/main/res/layout/activity_details.xml b/app/src/main/res/layout/fragment_details.xml
similarity index 97%
rename from app/src/main/res/layout/activity_details.xml
rename to app/src/main/res/layout/fragment_details.xml
index c74b41030..ad292428c 100644
--- a/app/src/main/res/layout/activity_details.xml
+++ b/app/src/main/res/layout/fragment_details.xml
@@ -23,7 +23,8 @@
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical">
+ android:orientation="vertical"
+ tools:context=".view.ui.details.AppDetailsFragment">
-
\ No newline at end of file
+
diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml
index b70490728..0d8dd5ced 100644
--- a/app/src/main/res/navigation/mobile_navigation.xml
+++ b/app/src/main/res/navigation/mobile_navigation.xml
@@ -98,4 +98,18 @@
android:name="com.aurora.store.view.ui.downloads.DownloadFragment"
android:label="@string/title_download_manager"
tools:layout="@layout/fragment_download" />
-
\ No newline at end of file
+
+
+
+
+
+