DO NOT MERGE: WIP: Migrate AppDetailsActivity to fragment

Signed-off-by: Aayush Gupta <aayushgupta219@gmail.com>
This commit is contained in:
Aayush Gupta
2023-07-03 18:09:40 +05:30
parent b01a9cacbd
commit 88d9dcdd53
12 changed files with 1320 additions and 1402 deletions

View File

@@ -92,43 +92,6 @@
android:name=".MainActivity"
android:launchMode="singleTask" />
<activity android:name=".view.ui.details.EmptyAppDetailsActivity"
android:noHistory="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="market" android:host="details" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="market.android.com" />
<data android:path="/store/apps/details" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="play.google.com" />
<data android:path="/store/apps/details" />
</intent-filter>
</activity>
<activity android:name=".view.ui.details.AppDetailsActivity"
android:exported="false">
</activity>
<activity android:name=".view.ui.details.DevProfileActivity"
android:exported="true">
<intent-filter>

View File

@@ -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),

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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())

View File

@@ -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) {

View File

@@ -1,846 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
* 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 <http://www.gnu.org/licenses/>.
*
*/
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<LinearLayout>
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<Download>) {
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<Download>) {
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<String>()
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<Runnable>()
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<DownloadBlock>,
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) {}
})
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,443 +0,0 @@
/*
* Aurora Store
* Copyright (C) 2021, Rahul Kumar Patel <whyorean@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
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<Review> {
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<Report> {
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<String, String> = 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")
}
}
}

View File

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

View File

@@ -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">
<LinearLayout
android:layout_width="match_parent"
@@ -120,4 +121,4 @@
<include
android:id="@+id/layout_details_install"
layout="@layout/layout_details_install" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -98,4 +98,18 @@
android:name="com.aurora.store.view.ui.downloads.DownloadFragment"
android:label="@string/title_download_manager"
tools:layout="@layout/fragment_download" />
</navigation>
<fragment
android:id="@+id/appDetailsFragment"
android:name="com.aurora.store.view.ui.details.AppDetailsFragment"
tools:layout="@layout/fragment_details" >
<argument
android:name="packageName"
app:argType="string" />
<deepLink
app:action="android.intent.action.VIEW"
app:uri="play.google.com/store/apps/details?id={packageName}" />
</fragment>
<action
android:id="@+id/action_global_appDetailsFragment"
app:destination="@id/appDetailsFragment" />
</navigation>