From 0e1ae6364d4a3195da647a827e73f0c6525930d1 Mon Sep 17 00:00:00 2001 From: Rahul Kumar Patel Date: Thu, 25 Feb 2021 08:57:30 +0530 Subject: [PATCH] Improve installer & related UI --- .../java/com/aurora/extensions/Commons.kt | 10 + .../com/aurora/store/AuroraApplication.kt | 4 + .../com/aurora/store/data/event/BusEvent.kt | 13 + .../store/data/installer/AppInstaller.kt | 4 +- .../store/data/installer/InstallerBase.kt | 10 +- .../store/data/installer/InstallerService.kt | 79 ++-- .../store/data/installer/RootInstaller.kt | 41 +- .../store/data/installer/ServiceInstaller.kt | 61 ++- .../store/data/installer/SessionInstaller.kt | 25 +- .../data/receiver/PackageManagerReceiver.kt | 2 +- .../store/data/service/NotificationService.kt | 49 ++- .../{ => layouts/button}/ActionButton.kt | 68 ++- .../{ => layouts/button}/StateButton.kt | 2 +- .../custom/progress/AuroraProgressView.java | 398 ++++++++++++++++++ .../progress/indicators/BallPulseIndicator.kt | 51 +++ .../custom/progress/indicators/Indicator.java | 191 +++++++++ .../view/ui/details/AppDetailsActivity.kt | 75 +++- .../store/view/ui/sheets/AppMenuSheet.kt | 3 +- .../store/view/ui/sheets/BaseBottomSheet.kt | 4 +- .../view/ui/sheets/InstallErrorDialogSheet.kt | 124 ++++++ .../store/view/ui/updates/UpdatesFragment.kt | 14 +- app/src/main/res/drawable/ic_install.xml | 28 ++ .../main/res/layout-land/activity_account.xml | 6 +- .../main/res/layout-land/activity_splash.xml | 4 +- .../res/layout-v21/layout_details_install.xml | 10 +- app/src/main/res/layout/activity_account.xml | 6 +- app/src/main/res/layout/activity_splash.xml | 4 +- .../res/layout/layout_details_install.xml | 10 +- .../main/res/layout/sheet_install_error.xml | 110 +++++ .../main/res/layout/view_action_button.xml | 78 ++-- .../main/res/menu/menu_download_single.xml | 2 +- app/src/main/res/values/attrs.xml | 18 + app/src/main/res/values/strings.xml | 9 +- app/src/main/res/values/styles_widget.xml | 24 ++ 34 files changed, 1379 insertions(+), 158 deletions(-) rename app/src/main/java/com/aurora/store/view/custom/{ => layouts/button}/ActionButton.kt (57%) rename app/src/main/java/com/aurora/store/view/custom/{ => layouts/button}/StateButton.kt (98%) create mode 100644 app/src/main/java/com/aurora/store/view/custom/progress/AuroraProgressView.java create mode 100644 app/src/main/java/com/aurora/store/view/custom/progress/indicators/BallPulseIndicator.kt create mode 100644 app/src/main/java/com/aurora/store/view/custom/progress/indicators/Indicator.java create mode 100644 app/src/main/java/com/aurora/store/view/ui/sheets/InstallErrorDialogSheet.kt create mode 100644 app/src/main/res/drawable/ic_install.xml create mode 100644 app/src/main/res/layout/sheet_install_error.xml diff --git a/app/src/main/java/com/aurora/extensions/Commons.kt b/app/src/main/java/com/aurora/extensions/Commons.kt index f2484f75f..8e6598acc 100644 --- a/app/src/main/java/com/aurora/extensions/Commons.kt +++ b/app/src/main/java/com/aurora/extensions/Commons.kt @@ -20,6 +20,8 @@ package com.aurora.extensions import android.text.format.DateFormat +import java.io.PrintWriter +import java.io.StringWriter import java.util.* fun Long.toDate(): String { @@ -27,3 +29,11 @@ fun Long.toDate(): String { calendar.timeInMillis = this return DateFormat.format("dd/MM/yy", calendar).toString() } + +fun Throwable.stackTraceToString(): String { + val stringWriter = StringWriter(1024) + val printWriter = PrintWriter(stringWriter) + printStackTrace(printWriter) + printWriter.close() + return stringWriter.toString() +} \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/AuroraApplication.kt b/app/src/main/java/com/aurora/store/AuroraApplication.kt index 8aeef42f6..f4196dcbd 100644 --- a/app/src/main/java/com/aurora/store/AuroraApplication.kt +++ b/app/src/main/java/com/aurora/store/AuroraApplication.kt @@ -36,6 +36,10 @@ class AuroraApplication : MultiDexApplication() { private lateinit var fetch: Fetch private lateinit var packageManagerReceiver: PackageManagerReceiver + companion object{ + val enqueuedInstalls: MutableSet = mutableSetOf() + } + override fun onCreate() { super.onCreate() diff --git a/app/src/main/java/com/aurora/store/data/event/BusEvent.kt b/app/src/main/java/com/aurora/store/data/event/BusEvent.kt index 32acba5e1..95ffd4df0 100644 --- a/app/src/main/java/com/aurora/store/data/event/BusEvent.kt +++ b/app/src/main/java/com/aurora/store/data/event/BusEvent.kt @@ -28,4 +28,17 @@ sealed class BusEvent { var email: String = String(), var aasToken: String = String() ) : BusEvent() +} + +sealed class SessionEvent { + data class Success( + var packageName: String? = "", + var extra: String? = "" + ) : SessionEvent() + + data class Failed( + var packageName: String? = "", + var error: String? = "", + var extra: String? = "" + ) : SessionEvent() } \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/data/installer/AppInstaller.kt b/app/src/main/java/com/aurora/store/data/installer/AppInstaller.kt index dd38a6587..1b6fe0f98 100644 --- a/app/src/main/java/com/aurora/store/data/installer/AppInstaller.kt +++ b/app/src/main/java/com/aurora/store/data/installer/AppInstaller.kt @@ -25,9 +25,7 @@ import com.aurora.store.data.SingletonHolder import com.aurora.store.util.Preferences import com.aurora.store.util.Preferences.PREFERENCE_INSTALLER_ID -open class AppInstaller private constructor(var context: Context) { - - companion object : SingletonHolder(::AppInstaller) +open class AppInstaller constructor(var context: Context) { fun getPreferredInstaller(): IInstaller { val prefValue = Preferences.getInteger( diff --git a/app/src/main/java/com/aurora/store/data/installer/InstallerBase.kt b/app/src/main/java/com/aurora/store/data/installer/InstallerBase.kt index 5a39f252b..f08c24a99 100644 --- a/app/src/main/java/com/aurora/store/data/installer/InstallerBase.kt +++ b/app/src/main/java/com/aurora/store/data/installer/InstallerBase.kt @@ -24,24 +24,22 @@ import android.content.Intent import android.net.Uri import android.os.Build import androidx.core.content.FileProvider +import com.aurora.store.AuroraApplication import com.aurora.store.BuildConfig import java.io.File -private val enqueuedInstalls: MutableSet = mutableSetOf() - abstract class InstallerBase(protected var context: Context) : IInstaller { - override fun clearQueue() { - enqueuedInstalls.clear() + AuroraApplication.enqueuedInstalls.clear() } override fun isAlreadyQueued(packageName: String): Boolean { - return enqueuedInstalls.contains(packageName) + return AuroraApplication.enqueuedInstalls.contains(packageName) } override fun removeFromInstallQueue(packageName: String) { - enqueuedInstalls.remove(packageName) + AuroraApplication.enqueuedInstalls.remove(packageName) } override fun uninstall(packageName: String) { diff --git a/app/src/main/java/com/aurora/store/data/installer/InstallerService.kt b/app/src/main/java/com/aurora/store/data/installer/InstallerService.kt index af0a3d2af..86283a885 100644 --- a/app/src/main/java/com/aurora/store/data/installer/InstallerService.kt +++ b/app/src/main/java/com/aurora/store/data/installer/InstallerService.kt @@ -25,52 +25,69 @@ import android.content.pm.PackageInstaller import android.os.Build import android.os.IBinder import androidx.annotation.RequiresApi -import org.apache.commons.lang3.StringUtils +import com.aurora.store.R +import com.aurora.store.data.event.SessionEvent +import com.aurora.store.util.Log +import org.greenrobot.eventbus.EventBus class InstallerService : Service() { @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1) + val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -69) val packageName = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME) - - //Send broadcast for the installation status of the package - sendStatusBroadcast(status, packageName) - - //Launch user confirmation activity - if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) { - val confirmationIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT) - confirmationIntent!!.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true) - confirmationIntent.putExtra( - Intent.EXTRA_INSTALLER_PACKAGE_NAME, - "com.android.vending" - ) - confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - try { - startActivity(confirmationIntent) - } catch (e: Exception) { - sendStatusBroadcast(PackageInstaller.STATUS_FAILURE, packageName) - } + val extra = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE); + when (status) { + PackageInstaller.STATUS_PENDING_USER_ACTION -> promptUser(intent) + else -> postStatus(status, packageName, extra) } + stopSelf() return START_NOT_STICKY } - @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - private fun sendStatusBroadcast(status: Int, packageName: String?) { - if (StringUtils.isNotEmpty(packageName)) { - val statusIntent = Intent(ACTION_SESSION_INSTALLER) - statusIntent.putExtra(PackageInstaller.EXTRA_STATUS, status) - statusIntent.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, packageName) - sendBroadcast(statusIntent) + private fun promptUser(intent: Intent) { + val confirmationIntent: Intent? = intent.getParcelableExtra(Intent.EXTRA_INTENT) + + confirmationIntent?.let { + it.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true) + it.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, "com.android.vending") + it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + try { + startActivity(it) + } catch (e: Exception) { + Log.e(e.message) + } + } + } + + private fun postStatus(status: Int, packageName: String?, extra: String?) { + when (status) { + PackageInstaller.STATUS_SUCCESS -> { + EventBus.getDefault().post(SessionEvent.Success(packageName, "Success")) + } + else -> { + val errorString = getErrorString(status) + Log.e("$packageName : $errorString") + EventBus.getDefault().post(SessionEvent.Failed(packageName, errorString, extra)) + } + } + } + + private fun getErrorString(status: Int): String { + return when (status) { + PackageInstaller.STATUS_FAILURE_ABORTED -> getString(R.string.installer_status_user_action) + PackageInstaller.STATUS_FAILURE_BLOCKED -> getString(R.string.installer_status_failure_blocked) + PackageInstaller.STATUS_FAILURE_CONFLICT -> getString(R.string.installer_status_failure_conflict) + PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> getString(R.string.installer_status_failure_incompatible) + PackageInstaller.STATUS_FAILURE_INVALID -> getString(R.string.installer_status_failure_invalid) + PackageInstaller.STATUS_FAILURE_STORAGE -> getString(R.string.installer_status_failure_storage) + else -> getString(R.string.installer_status_failure) } } override fun onBind(intent: Intent): IBinder? { return null } - - companion object { - private const val ACTION_SESSION_INSTALLER = "ACTION_SESSION_INSTALLER" - } } \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/data/installer/RootInstaller.kt b/app/src/main/java/com/aurora/store/data/installer/RootInstaller.kt index 3e4f4a7b5..0f734ecc8 100644 --- a/app/src/main/java/com/aurora/store/data/installer/RootInstaller.kt +++ b/app/src/main/java/com/aurora/store/data/installer/RootInstaller.kt @@ -20,11 +20,13 @@ package com.aurora.store.data.installer import android.content.Context -import com.aurora.store.R -import com.aurora.store.util.Log import com.aurora.extensions.isLAndAbove import com.aurora.extensions.toast +import com.aurora.store.R +import com.aurora.store.data.event.SessionEvent +import com.aurora.store.util.Log import com.topjohnwu.superuser.Shell +import org.greenrobot.eventbus.EventBus import java.io.File import java.util.regex.Pattern @@ -50,7 +52,12 @@ class RootInstaller(context: Context) : InstallerBase(context) { xInstallLegacy(packageName, it) } } else { - context.toast(context.getString(R.string.installer_root_unavailable)) + val event = SessionEvent.Failed( + packageName, + context.getString(R.string.installer_status_failure), + context.getString(R.string.installer_root_unavailable) + ) + EventBus.getDefault().post(event) Log.e(" >>>>>>>>>>>>>>>>>>>>>>>>>> NO ROOT ACCESS <<<<<<<<<<<<<<<<<<<<<<<<<<<<<") } } @@ -80,12 +87,34 @@ class RootInstaller(context: Context) : InstallerBase(context) { .exec() } - Shell.su("pm install-commit $sessionId").exec() + val shellResult = Shell.su("pm install-commit $sessionId").exec() + + if (!shellResult.isSuccess) { + removeFromInstallQueue(packageName) + val event = SessionEvent.Failed( + packageName, + context.getString(R.string.installer_status_failure), + parseError(shellResult) + ) + EventBus.getDefault().post(event) + } } else { removeFromInstallQueue(packageName) + val event = SessionEvent.Failed( + packageName, + context.getString(R.string.installer_status_failure), + context.getString(R.string.installer_root_unavailable) + ) + EventBus.getDefault().post(event) } } else { removeFromInstallQueue(packageName) + val event = SessionEvent.Failed( + packageName, + context.getString(R.string.installer_status_failure), + context.getString(R.string.installer_status_failure_session) + ) + EventBus.getDefault().post(event) } } @@ -96,4 +125,8 @@ class RootInstaller(context: Context) : InstallerBase(context) { removeFromInstallQueue(packageName) } } + + private fun parseError(result: Shell.Result): String { + return result.err.joinToString(separator = "\n") + } } \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/data/installer/ServiceInstaller.kt b/app/src/main/java/com/aurora/store/data/installer/ServiceInstaller.kt index 567b98ea4..5168a5f6b 100644 --- a/app/src/main/java/com/aurora/store/data/installer/ServiceInstaller.kt +++ b/app/src/main/java/com/aurora/store/data/installer/ServiceInstaller.kt @@ -32,7 +32,10 @@ import androidx.core.content.FileProvider import com.aurora.services.IPrivilegedCallback import com.aurora.services.IPrivilegedService import com.aurora.store.BuildConfig +import com.aurora.store.R +import com.aurora.store.data.event.SessionEvent import com.aurora.store.util.Log +import org.greenrobot.eventbus.EventBus import java.io.File class ServiceInstaller(context: Context) : InstallerBase(context) { @@ -65,27 +68,51 @@ class ServiceInstaller(context: Context) : InstallerBase(context) { @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun xInstall(packageName: String, uriList: List) { - val serviceConnection = object : ServiceConnection { override fun onServiceConnected(name: ComponentName, binder: IBinder) { val service = IPrivilegedService.Stub.asInterface(binder) - val callback = object : IPrivilegedCallback.Stub() { - override fun handleResult(packageName: String, returnCode: Int) { - removeFromInstallQueue(packageName) - } - } - try { - service.installSplitPackage( - packageName, - uriList, - ACTION_INSTALL_REPLACE_EXISTING, - BuildConfig.APPLICATION_ID, - callback - ) - } catch (e: RemoteException) { - removeFromInstallQueue(packageName) - Log.e("Failed to connect Aurora Services") + if (service.hasPrivilegedPermissions()) { + Log.i(context.getString(R.string.installer_service_available)) + val callback = object : IPrivilegedCallback.Stub() { + override fun handleResult(packageName: String, returnCode: Int) { + Log.e("$packageName : $returnCode") + removeFromInstallQueue(packageName) + } + } + + try { + service.installSplitPackage( + packageName, + uriList, + ACTION_INSTALL_REPLACE_EXISTING, + BuildConfig.APPLICATION_ID, + callback + ) + } catch (e: RemoteException) { + removeFromInstallQueue(packageName) + EventBus + .getDefault() + .post( + SessionEvent.Failed( + packageName, + e.localizedMessage, + e.stackTraceToString() + ) + ) + Log.e("Failed to connect Aurora Services") + } + } else { + Log.e(context.getString(R.string.installer_service_unavailable)) + EventBus + .getDefault() + .post( + SessionEvent.Failed( + packageName, + context.getString(R.string.installer_status_failure), + context.getString(R.string.installer_service_unavailable) + ) + ) } } diff --git a/app/src/main/java/com/aurora/store/data/installer/SessionInstaller.kt b/app/src/main/java/com/aurora/store/data/installer/SessionInstaller.kt index 70cd5c788..70c5125ae 100644 --- a/app/src/main/java/com/aurora/store/data/installer/SessionInstaller.kt +++ b/app/src/main/java/com/aurora/store/data/installer/SessionInstaller.kt @@ -27,9 +27,12 @@ import android.net.Uri import android.os.Build import androidx.annotation.RequiresApi import androidx.core.content.FileProvider +import com.aurora.extensions.isNAndAbove import com.aurora.store.BuildConfig +import com.aurora.store.data.event.SessionEvent import com.aurora.store.util.Log import org.apache.commons.io.IOUtils +import org.greenrobot.eventbus.EventBus import java.io.File class SessionInstaller(context: Context) : InstallerBase(context) { @@ -57,18 +60,22 @@ class SessionInstaller(context: Context) : InstallerBase(context) { @RequiresApi(Build.VERSION_CODES.LOLLIPOP) private fun xInstall(packageName: String, uriList: List) { val packageInstaller = context.packageManager.packageInstaller - val sessionParams = SessionParams(SessionParams.MODE_FULL_INSTALL) + val sessionParams = SessionParams(SessionParams.MODE_FULL_INSTALL).apply { + setAppPackageName(packageName) + if (isNAndAbove()) { + setOriginatingUid(android.os.Process.myUid()) + } + } val sessionId = packageInstaller.createSession(sessionParams) val session = packageInstaller.openSession(sessionId) try { - Log.i("Writing splits to session for $packageName") - var apkId = 1 + for (uri in uriList) { val inputStream = context.contentResolver.openInputStream(uri) val outputStream = session.openWrite( - "${packageName}_${apkId++}", + "${packageName}_${System.currentTimeMillis()}", 0, -1 ) @@ -81,11 +88,11 @@ class SessionInstaller(context: Context) : InstallerBase(context) { IOUtils.close(outputStream) } - val intent = Intent(context, InstallerService::class.java) + val callBackIntent = Intent(context, InstallerService::class.java) val pendingIntent = PendingIntent.getService( context, sessionId, - intent, + callBackIntent, PendingIntent.FLAG_UPDATE_CURRENT ) @@ -95,6 +102,12 @@ class SessionInstaller(context: Context) : InstallerBase(context) { } catch (e: Exception) { session.abandon() removeFromInstallQueue(packageName) + val event = SessionEvent.Failed( + packageName, + e.localizedMessage, + e.stackTraceToString() + ) + EventBus.getDefault().post(event) Log.e("Failed to install $packageName : %s", e.message) } } diff --git a/app/src/main/java/com/aurora/store/data/receiver/PackageManagerReceiver.kt b/app/src/main/java/com/aurora/store/data/receiver/PackageManagerReceiver.kt index 8e2703657..c78fcc70a 100644 --- a/app/src/main/java/com/aurora/store/data/receiver/PackageManagerReceiver.kt +++ b/app/src/main/java/com/aurora/store/data/receiver/PackageManagerReceiver.kt @@ -43,7 +43,7 @@ open class PackageManagerReceiver : BroadcastReceiver() { .post(InstallEvent(packageName, "")) //Clear installation queue - AppInstaller.with(context) + AppInstaller(context) .getPreferredInstaller() .removeFromInstallQueue(packageName) } 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 c5d02b788..591048cda 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,10 +29,12 @@ import android.util.ArrayMap import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import com.aurora.Constants +import com.aurora.extensions.getStyledAttributeColor import com.aurora.extensions.isLAndAbove import com.aurora.gplayapi.data.models.App import com.aurora.store.R import com.aurora.store.data.downloader.DownloadManager +import com.aurora.store.data.event.SessionEvent import com.aurora.store.data.installer.AppInstaller import com.aurora.store.data.receiver.DownloadCancelReceiver import com.aurora.store.data.receiver.DownloadPauseReceiver @@ -46,6 +48,8 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import com.tonyodev.fetch2.* import org.apache.commons.lang3.StringUtils +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe import java.lang.reflect.Modifier import java.util.* @@ -83,6 +87,8 @@ class NotificationService : Service() { Log.i("Notification Service Started") + EventBus.getDefault().register(this) + notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager //Create Notification Channels : General & Alert @@ -97,9 +103,6 @@ class NotificationService : Service() { override fun onCompleted(groupId: Int, download: Download, fetchGroup: FetchGroup) { showNotification(groupId, download, fetchGroup) - if (fetchGroup.groupDownloadProgress == 100) { - install(download.tag!!, fetchGroup.downloads) - } } override fun onError( @@ -214,8 +217,8 @@ class NotificationService : Service() { } val progress = fetchGroup.groupDownloadProgress val progressBigText = NotificationCompat.BigTextStyle() - when (status) { + when (status) { Status.QUEUED -> { builder.setProgress(100, 0, true) progressBigText.bigText(getString(R.string.download_queued)) @@ -308,7 +311,6 @@ class NotificationService : Service() { app.id, builder.build() ) - //} } private fun getPauseIntent(groupId: Int): PendingIntent { @@ -356,9 +358,28 @@ class NotificationService : Service() { ) } + @Subscribe() + fun onEventMainThread(event: Any) { + when (event) { + is SessionEvent.Success -> { + val app = appMap[event.packageName.hashCode()] + if (app != null) + notifyInstallationStatus(app, event.extra) + } + is SessionEvent.Failed -> { + val app = appMap[event.packageName.hashCode()] + if (app != null) + notifyInstallationStatus(app, event.error) + } + else -> { + + } + } + } + @Synchronized private fun install(packageName: String, files: List) { - AppInstaller.with(this) + AppInstaller(this) .getPreferredInstaller() .install( packageName, @@ -370,9 +391,25 @@ class NotificationService : Service() { ) } + private fun notifyInstallationStatus(app: App, status: String?) { + val builder = NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL_ALERT) + builder.color = getStyledAttributeColor(R.attr.colorAccent) + builder.setSmallIcon(R.drawable.ic_install) + builder.setContentTitle(app.displayName) + builder.setContentText(status) + builder.setSubText(app.packageName) + + notificationManager.notify( + app.packageName, + app.id, + builder.build() + ) + } + override fun onDestroy() { Log.i("Notification Service Stopped") fetch.removeListener(fetchListener) + EventBus.getDefault().unregister(this); super.onDestroy() } } \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/view/custom/ActionButton.kt b/app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt similarity index 57% rename from app/src/main/java/com/aurora/store/view/custom/ActionButton.kt rename to app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt index 53bbe5ed6..5e6a16424 100644 --- a/app/src/main/java/com/aurora/store/view/custom/ActionButton.kt +++ b/app/src/main/java/com/aurora/store/view/custom/layouts/button/ActionButton.kt @@ -17,16 +17,22 @@ * */ -package com.aurora.store.view.custom +package com.aurora.store.view.custom.layouts.button import android.content.Context +import android.content.res.ColorStateList import android.os.Build import android.util.AttributeSet -import android.view.View import android.widget.RelativeLayout import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import com.aurora.extensions.getString +import com.aurora.extensions.isLAndAbove +import com.aurora.extensions.runOnUiThread import com.aurora.store.R import com.aurora.store.databinding.ViewActionButtonBinding +import nl.komponents.kovenant.task +import java.util.concurrent.TimeUnit class ActionButton : RelativeLayout { @@ -64,23 +70,71 @@ class ActionButton : RelativeLayout { val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ActionButton) val btnTxt = typedArray.getString(R.styleable.ActionButton_btnActionText) + + val btnTxtColor = typedArray.getResourceId( + R.styleable.ActionButton_btnActionTextColor, + R.color.colorWhite + ) + + val stateIcon = typedArray.getResourceId( + R.styleable.ActionButton_btnActionIcon, + R.drawable.ic_check + ) + + val stateColor = ContextCompat.getColor(context, btnTxtColor) + B.btn.text = btnTxt + B.btn.setTextColor(stateColor) + B.img.setImageDrawable(ContextCompat.getDrawable(context, stateIcon)) + if (isLAndAbove()) { + B.img.imageTintList = ColorStateList.valueOf(stateColor) + } + typedArray.recycle() } fun setText(text: String) { + B.viewFlipper.displayedChild = 0 B.btn.text = text } - fun updateProgress(isVisible: Boolean) { - if (isVisible) - B.progress.visibility = View.VISIBLE - else + fun setText(text: Int) { + B.viewFlipper.displayedChild = 0 + B.btn.text = getString(text) + } - B.progress.visibility = View.INVISIBLE + fun updateState(state: State) { + val displayChild = when (state) { + State.IDLE -> 0 + State.PROGRESS -> 1 + State.COMPLETE -> 2 + } + + if (B.viewFlipper.displayedChild != displayChild) { + runOnUiThread { + B.viewFlipper.displayedChild = displayChild + + if (displayChild == 2) + switchToIdle() + } + } + } + + private fun switchToIdle() { + task { + TimeUnit.SECONDS.sleep(3) + } success { + updateState(State.IDLE) + } } fun addOnClickListener(onClickListener: OnClickListener) { B.btn.setOnClickListener(onClickListener) } + + enum class State { + IDLE, + PROGRESS, + COMPLETE, + } } \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/view/custom/StateButton.kt b/app/src/main/java/com/aurora/store/view/custom/layouts/button/StateButton.kt similarity index 98% rename from app/src/main/java/com/aurora/store/view/custom/StateButton.kt rename to app/src/main/java/com/aurora/store/view/custom/layouts/button/StateButton.kt index 91b05954f..ed480e896 100644 --- a/app/src/main/java/com/aurora/store/view/custom/StateButton.kt +++ b/app/src/main/java/com/aurora/store/view/custom/layouts/button/StateButton.kt @@ -17,7 +17,7 @@ * */ -package com.aurora.store.view.custom +package com.aurora.store.view.custom.layouts.button import android.content.Context import android.os.Build diff --git a/app/src/main/java/com/aurora/store/view/custom/progress/AuroraProgressView.java b/app/src/main/java/com/aurora/store/view/custom/progress/AuroraProgressView.java new file mode 100644 index 000000000..8459192e4 --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/custom/progress/AuroraProgressView.java @@ -0,0 +1,398 @@ +package com.aurora.store.view.custom.progress; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; +import android.view.animation.AnimationUtils; + +import com.aurora.store.R; +import com.aurora.store.view.custom.progress.indicators.BallPulseIndicator; +import com.aurora.store.view.custom.progress.indicators.Indicator; + + +public class AuroraProgressView extends View { + + private static final String TAG = "AuroraProgressView"; + + private static final BallPulseIndicator DEFAULT_INDICATOR = new BallPulseIndicator(); + + private static final int MIN_SHOW_TIME = 500; // ms + private static final int MIN_DELAY = 500; // ms + + private long startTime = -1; + + private boolean postedHide = false; + private boolean postedShow = false; + private boolean dismissed = false; + + private final Runnable mDelayedHide = new Runnable() { + + @Override + public void run() { + postedHide = false; + startTime = -1; + setVisibility(View.GONE); + } + }; + + private final Runnable mDelayedShow = new Runnable() { + + @Override + public void run() { + postedShow = false; + if (!dismissed) { + startTime = System.currentTimeMillis(); + setVisibility(View.VISIBLE); + } + } + }; + + int minWidth; + int maxWidth; + int minHeight; + int maxHeight; + + private Indicator indicator; + private int indicatorColor; + + private boolean mShouldStartAnimationDrawable; + + public AuroraProgressView(Context context) { + super(context); + init(context, null, 0, 0); + } + + public AuroraProgressView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, 0, R.style.AuroraProgressView); + } + + public AuroraProgressView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context, attrs, defStyleAttr, R.style.AuroraProgressView); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public AuroraProgressView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(context, attrs, defStyleAttr, R.style.AuroraProgressView); + } + + private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + minWidth = 24; + maxWidth = 48; + minHeight = 24; + maxHeight = 48; + + final TypedArray a = context.obtainStyledAttributes( + attrs, R.styleable.AuroraProgressView, defStyleAttr, defStyleRes); + + minWidth = a.getDimensionPixelSize(R.styleable.AuroraProgressView_minWidth, minWidth); + maxWidth = a.getDimensionPixelSize(R.styleable.AuroraProgressView_maxWidth, maxWidth); + minHeight = a.getDimensionPixelSize(R.styleable.AuroraProgressView_minHeight, minHeight); + maxHeight = a.getDimensionPixelSize(R.styleable.AuroraProgressView_maxHeight, maxHeight); + String indicatorName = a.getString(R.styleable.AuroraProgressView_indicatorName); + indicatorColor = a.getColor(R.styleable.AuroraProgressView_indicatorColor, Color.WHITE); + setIndicator(indicatorName); + + if (indicator == null) { + setIndicator(DEFAULT_INDICATOR); + } + + a.recycle(); + } + + public Indicator getIndicator() { + return indicator; + } + + public void setIndicator(Indicator d) { + if (indicator != d) { + if (indicator != null) { + indicator.setCallback(null); + unscheduleDrawable(indicator); + } + + indicator = d; + //need to set indicator color again if you didn't specified when you update the indicator . + setIndicatorColor(indicatorColor); + if (d != null) { + d.setCallback(this); + } + postInvalidate(); + } + } + + public void setIndicatorColor(int color) { + this.indicatorColor = color; + indicator.setColor(color); + } + + public void setIndicator(String indicatorName) { + if (TextUtils.isEmpty(indicatorName)) { + return; + } + StringBuilder drawableClassName = new StringBuilder(); + if (!indicatorName.contains(".")) { + String defaultPackageName = getClass().getPackage().getName(); + drawableClassName.append(defaultPackageName) + .append(".indicators") + .append("."); + } + drawableClassName.append(indicatorName); + try { + Class drawableClass = Class.forName(drawableClassName.toString()); + Indicator indicator = (Indicator) drawableClass.newInstance(); + setIndicator(indicator); + } catch (ClassNotFoundException e) { + Log.e(TAG, "Didn't find your class , check the name again !"); + } catch (InstantiationException | IllegalAccessException e) { + e.printStackTrace(); + } + } + + public void smoothToShow() { + startAnimation(AnimationUtils.loadAnimation(getContext(), android.R.anim.fade_in)); + setVisibility(VISIBLE); + } + + public void smoothToHide() { + startAnimation(AnimationUtils.loadAnimation(getContext(), android.R.anim.fade_out)); + setVisibility(GONE); + } + + public void hide() { + dismissed = true; + removeCallbacks(mDelayedShow); + long diff = System.currentTimeMillis() - startTime; + if (diff >= MIN_SHOW_TIME || startTime == -1) { + // The progress spinner has been shown long enough + // OR was not shown yet. If it wasn't shown yet, + // it will just never be shown. + setVisibility(View.GONE); + } else { + // The progress spinner is shown, but not long enough, + // so put a delayed message in to hide it when its been + // shown long enough. + if (!postedHide) { + postDelayed(mDelayedHide, MIN_SHOW_TIME - diff); + postedHide = true; + } + } + } + + public void show() { + // Reset the start time. + startTime = -1; + dismissed = false; + removeCallbacks(mDelayedHide); + if (!postedShow) { + postDelayed(mDelayedShow, MIN_DELAY); + postedShow = true; + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return who == indicator + || super.verifyDrawable(who); + } + + void startAnimation() { + if (getVisibility() != VISIBLE) { + return; + } + + if (indicator != null) { + mShouldStartAnimationDrawable = true; + } + postInvalidate(); + } + + void stopAnimation() { + if (indicator != null) { + indicator.stop(); + mShouldStartAnimationDrawable = false; + } + postInvalidate(); + } + + @Override + public void setVisibility(int v) { + if (getVisibility() != v) { + super.setVisibility(v); + if (v == GONE || v == INVISIBLE) { + stopAnimation(); + } else { + startAnimation(); + } + } + } + + @Override + protected void onVisibilityChanged(View changedView, int visibility) { + super.onVisibilityChanged(changedView, visibility); + if (visibility == GONE || visibility == INVISIBLE) { + stopAnimation(); + } else { + startAnimation(); + } + } + + @Override + public void invalidateDrawable(Drawable dr) { + if (verifyDrawable(dr)) { + final Rect dirty = dr.getBounds(); + final int scrollX = getScrollX() + getPaddingLeft(); + final int scrollY = getScrollY() + getPaddingTop(); + + invalidate(dirty.left + scrollX, dirty.top + scrollY, + dirty.right + scrollX, dirty.bottom + scrollY); + } else { + super.invalidateDrawable(dr); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + updateDrawableBounds(w, h); + } + + private void updateDrawableBounds(int w, int h) { + // onDraw will translate the canvas so we draw starting at 0,0. + // Subtract out padding for the purposes of the calculations below. + w -= getPaddingRight() + getPaddingLeft(); + h -= getPaddingTop() + getPaddingBottom(); + + int right = w; + int bottom = h; + int top = 0; + int left = 0; + + if (indicator != null) { + // Maintain aspect ratio. Certain kinds of animated drawables + // get very confused otherwise. + final int intrinsicWidth = indicator.getIntrinsicWidth(); + final int intrinsicHeight = indicator.getIntrinsicHeight(); + final float intrinsicAspect = (float) intrinsicWidth / intrinsicHeight; + final float boundAspect = (float) w / h; + if (intrinsicAspect != boundAspect) { + if (boundAspect > intrinsicAspect) { + // New width is larger. Make it smaller to match height. + final int width = (int) (h * intrinsicAspect); + left = (w - width) / 2; + right = left + width; + } else { + // New height is larger. Make it smaller to match width. + final int height = (int) (w * (1 / intrinsicAspect)); + top = (h - height) / 2; + bottom = top + height; + } + } + indicator.setBounds(left, top, right, bottom); + } + } + + @Override + protected synchronized void onDraw(Canvas canvas) { + super.onDraw(canvas); + drawTrack(canvas); + } + + void drawTrack(Canvas canvas) { + final Drawable d = indicator; + if (d != null) { + // Translate canvas so a indeterminate circular progress bar with padding + // rotates properly in its animation + final int saveCount = canvas.save(); + + canvas.translate(getPaddingLeft(), getPaddingTop()); + + d.draw(canvas); + canvas.restoreToCount(saveCount); + + if (mShouldStartAnimationDrawable && d instanceof Animatable) { + ((Animatable) d).start(); + mShouldStartAnimationDrawable = false; + } + } + } + + @Override + protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int dw = 0; + int dh = 0; + + final Drawable d = indicator; + if (d != null) { + dw = Math.max(minWidth, Math.min(maxWidth, d.getIntrinsicWidth())); + dh = Math.max(minHeight, Math.min(maxHeight, d.getIntrinsicHeight())); + } + + updateDrawableState(); + + dw += getPaddingLeft() + getPaddingRight(); + dh += getPaddingTop() + getPaddingBottom(); + + final int measuredWidth = resolveSizeAndState(dw, widthMeasureSpec, 0); + final int measuredHeight = resolveSizeAndState(dh, heightMeasureSpec, 0); + setMeasuredDimension(measuredWidth, measuredHeight); + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + updateDrawableState(); + } + + private void updateDrawableState() { + final int[] state = getDrawableState(); + if (indicator != null && indicator.isStateful()) { + indicator.setState(state); + } + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public void drawableHotspotChanged(float x, float y) { + super.drawableHotspotChanged(x, y); + + if (indicator != null) { + indicator.setHotspot(x, y); + } + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + startAnimation(); + removeCallbacks(); + } + + @Override + protected void onDetachedFromWindow() { + stopAnimation(); + // This should come after stopAnimation(), otherwise an invalidate message remains in the + // queue, which can prevent the entire view hierarchy from being GC'ed during a rotation + super.onDetachedFromWindow(); + removeCallbacks(); + } + + private void removeCallbacks() { + removeCallbacks(mDelayedHide); + removeCallbacks(mDelayedShow); + } + + +} + diff --git a/app/src/main/java/com/aurora/store/view/custom/progress/indicators/BallPulseIndicator.kt b/app/src/main/java/com/aurora/store/view/custom/progress/indicators/BallPulseIndicator.kt new file mode 100644 index 000000000..61a0af1bc --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/custom/progress/indicators/BallPulseIndicator.kt @@ -0,0 +1,51 @@ +package com.aurora.store.view.custom.progress.indicators + +import android.animation.ValueAnimator +import android.graphics.Canvas +import android.graphics.Paint +import java.util.* + +class BallPulseIndicator : Indicator() { + + private val scaleFloats = floatArrayOf( + 0.75f, + 1.0f, + 0.75f + ) + + override fun draw(canvas: Canvas, paint: Paint) { + val circleSpacing = 4f + val radius = (width.coerceAtMost(height) - circleSpacing * 2) / 6 + val x = width / 2f - (radius * 2 + circleSpacing) + val y = height / 2f + + for (i in 0..2) { + canvas.save() + val translateX = x + radius * 2 * i + circleSpacing * i + canvas.translate(translateX, y) + canvas.scale(scaleFloats[i], scaleFloats[i]) + canvas.drawCircle(0f, 0f, radius, paint) + canvas.restore() + } + } + + override fun onCreateAnimators(): ArrayList { + val animators = ArrayList() + val delays = intArrayOf(120, 240, 360) + + for (i in 0..2) { + val scaleAnim = ValueAnimator.ofFloat(1f, 0.3f, 1f) + scaleAnim.duration = 750 + scaleAnim.repeatCount = -1 + scaleAnim.startDelay = delays[i].toLong() + + addUpdateListener(scaleAnim) { animation: ValueAnimator -> + scaleFloats[i] = animation.animatedValue as Float + postInvalidate() + } + + animators.add(scaleAnim) + } + return animators + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aurora/store/view/custom/progress/indicators/Indicator.java b/app/src/main/java/com/aurora/store/view/custom/progress/indicators/Indicator.java new file mode 100644 index 000000000..4e3104664 --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/custom/progress/indicators/Indicator.java @@ -0,0 +1,191 @@ +package com.aurora.store.view.custom.progress.indicators; + +import android.animation.ValueAnimator; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; + +import java.util.ArrayList; +import java.util.HashMap; + +public abstract class Indicator extends Drawable implements Animatable { + + private static final Rect ZERO_BOUNDS_RECT = new Rect(); + + private final HashMap updateListenerHashMap = new HashMap<>(); + private final Paint paint = new Paint(); + + private int alpha = 255; + private ArrayList animators; + private boolean hasAnimators; + + protected Rect drawBounds = ZERO_BOUNDS_RECT; + + public Indicator() { + paint.setColor(Color.WHITE); + paint.setStyle(Paint.Style.FILL); + paint.setAntiAlias(true); + } + + public int getColor() { + return paint.getColor(); + } + + public void setColor(int color) { + paint.setColor(color); + } + + @Override + public void setAlpha(int alpha) { + this.alpha = alpha; + } + + @Override + public int getAlpha() { + return alpha; + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + + } + + @Override + public void draw(Canvas canvas) { + draw(canvas, paint); + } + + public abstract void draw(Canvas canvas, Paint paint); + + public abstract ArrayList onCreateAnimators(); + + @Override + public void start() { + ensureAnimators(); + + if (animators == null) { + return; + } + + // If the animators has not ended, do nothing. + if (isStarted()) { + return; + } + startAnimators(); + invalidateSelf(); + } + + private void startAnimators() { + for (int i = 0; i < animators.size(); i++) { + ValueAnimator animator = animators.get(i); + + //when the animator restart , add the updateListener again because they + // was removed by animator stop . + ValueAnimator.AnimatorUpdateListener updateListener = updateListenerHashMap.get(animator); + if (updateListener != null) { + animator.addUpdateListener(updateListener); + } + + animator.start(); + } + } + + private void stopAnimators() { + if (animators != null) { + for (ValueAnimator animator : animators) { + if (animator != null && animator.isStarted()) { + animator.removeAllUpdateListeners(); + animator.end(); + } + } + } + } + + private void ensureAnimators() { + if (!hasAnimators) { + animators = onCreateAnimators(); + hasAnimators = true; + } + } + + @Override + public void stop() { + stopAnimators(); + } + + private boolean isStarted() { + for (ValueAnimator animator : animators) { + return animator.isStarted(); + } + return false; + } + + @Override + public boolean isRunning() { + for (ValueAnimator animator : animators) { + return animator.isRunning(); + } + return false; + } + + public void addUpdateListener(ValueAnimator animator, ValueAnimator.AnimatorUpdateListener updateListener) { + updateListenerHashMap.put(animator, updateListener); + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + setDrawBounds(bounds); + } + + public void setDrawBounds(Rect drawBounds) { + setDrawBounds(drawBounds.left, drawBounds.top, drawBounds.right, drawBounds.bottom); + } + + public void setDrawBounds(int left, int top, int right, int bottom) { + this.drawBounds = new Rect(left, top, right, bottom); + } + + public void postInvalidate() { + invalidateSelf(); + } + + public Rect getDrawBounds() { + return drawBounds; + } + + public int getWidth() { + return drawBounds.width(); + } + + public int getHeight() { + return drawBounds.height(); + } + + public int centerX() { + return drawBounds.centerX(); + } + + public int centerY() { + return drawBounds.centerY(); + } + + public float exactCenterX() { + return drawBounds.exactCenterX(); + } + + public float exactCenterY() { + return drawBounds.exactCenterY(); + } + +} 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 index 9a754acac..d53ee950c 100644 --- 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 @@ -29,9 +29,7 @@ import android.view.MenuItem import android.view.View import android.widget.LinearLayout import com.aurora.Constants -import com.aurora.extensions.isLAndAbove -import com.aurora.extensions.load -import com.aurora.extensions.toast +import com.aurora.extensions.* import com.aurora.gplayapi.data.models.App import com.aurora.gplayapi.data.models.AuthData import com.aurora.gplayapi.data.models.File @@ -42,13 +40,15 @@ import com.aurora.store.R import com.aurora.store.data.downloader.DownloadManager import com.aurora.store.data.downloader.RequestBuilder import com.aurora.store.data.event.BusEvent +import com.aurora.store.data.event.SessionEvent 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.databinding.ActivityDetailsBinding import com.aurora.store.util.* -import com.aurora.extensions.* +import com.aurora.store.view.custom.layouts.button.ActionButton import com.aurora.store.view.ui.downloads.DownloadActivity +import com.aurora.store.view.ui.sheets.InstallErrorDialogSheet import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback @@ -102,13 +102,29 @@ class AppDetailsActivity : BaseDetailsActivity() { } @Subscribe(threadMode = ThreadMode.MAIN) - fun onEventMainThread(event: BusEvent) { + fun onEventMainThread(event: Any) { when (event) { is BusEvent.InstallEvent -> { - attachActions() + if (app.packageName == event.packageName) { + attachActions() + } } is BusEvent.UninstallEvent -> { - attachActions() + if (app.packageName == event.packageName) { + attachActions() + } + } + is SessionEvent.Failed -> { + if (app.packageName == event.packageName) { + InstallErrorDialogSheet.newInstance( + app, + event.packageName, + event.error, + event.extra + ).show(supportFragmentManager, "SED") + attachActions() + updateActionState(ActionButton.State.IDLE) + } } else -> { @@ -208,6 +224,10 @@ class AppDetailsActivity : BaseDetailsActivity() { checkAndSetupInstall() } + private fun updateActionState(state: ActionButton.State) { + B.layoutDetailsInstall.btnDownload.updateState(state) + } + private fun openApp() { val intent = PackageUtil.getLaunchIntent(this, app.packageName) if (intent != null) { @@ -221,8 +241,10 @@ class AppDetailsActivity : BaseDetailsActivity() { @Synchronized private fun install(files: List) { + updateActionState(ActionButton.State.IDLE) + task { - AppInstaller.with(this) + AppInstaller(this) .getPreferredInstaller() .install( app.packageName, @@ -241,7 +263,7 @@ class AppDetailsActivity : BaseDetailsActivity() { @Synchronized private fun uninstallApp() { - AppInstaller.with(this) + AppInstaller(this) .getPreferredInstaller() .uninstall(app.packageName) } @@ -363,6 +385,7 @@ class AppDetailsActivity : BaseDetailsActivity() { } private fun purchase() { + updateActionState(ActionButton.State.PROGRESS) task { val authData = AuthProvider .with(this) @@ -376,9 +399,11 @@ class AppDetailsActivity : BaseDetailsActivity() { enqueue(it) } else { Log.e("Failed to download : ${app.displayName}") + updateActionState(ActionButton.State.IDLE) } } failUi { expandBottomSheet(it.message) + updateActionState(ActionButton.State.IDLE) Log.e("Failed to purchase ${app.displayName} : ${it.message}") } } @@ -402,6 +427,7 @@ class AppDetailsActivity : BaseDetailsActivity() { Log.i("Downloading Apks : %s", app.displayName) } } else { + updateActionState(ActionButton.State.IDLE) expandBottomSheet(getString(R.string.purchase_no_file)) } } @@ -443,8 +469,14 @@ class AppDetailsActivity : BaseDetailsActivity() { bottomSheetBehavior.isHideable = false bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED - B.layoutDetailsInstall.txtPurchaseError.text = message - B.layoutDetailsInstall.btnDownload.updateProgress(false) + with(B.layoutDetailsInstall) { + txtPurchaseError.text = message + btnDownload.updateState(ActionButton.State.IDLE) + if (app.isFree) + btnDownload.setText(R.string.action_install) + else + btnDownload.setText(app.price) + } } private fun checkAndSetupInstall() { @@ -459,39 +491,40 @@ class AppDetailsActivity : BaseDetailsActivity() { ) if (isUpdatable) { - btn.setText(getString(R.string.action_update)) + btn.setText(R.string.action_update) btn.addOnClickListener { - btn.updateProgress(true) startDownload() } } else { - btn.setText(getString(R.string.action_open)) + btn.setText(R.string.action_open) btn.addOnClickListener { openApp() } } } else { if (app.isFree) { - btn.setText(getString(R.string.action_install)) + btn.setText(R.string.action_install) } else { btn.setText(app.price) } btn.addOnClickListener { - btn.setText(getString(R.string.download_metadata)) - btn.updateProgress(true) + btn.setText(R.string.download_metadata) startDownload() } } - btn.updateProgress(false) + btn.updateState(ActionButton.State.IDLE) } } @Synchronized private fun flip(nextView: Int) { runOnUiThread { - B.layoutDetailsInstall.viewFlipper.displayedChild = nextView - if (nextView == 0) - checkAndSetupInstall() + val displayChild = B.layoutDetailsInstall.viewFlipper.displayedChild + if (displayChild != nextView) { + B.layoutDetailsInstall.viewFlipper.displayedChild = nextView + if (nextView == 0) + checkAndSetupInstall() + } } } diff --git a/app/src/main/java/com/aurora/store/view/ui/sheets/AppMenuSheet.kt b/app/src/main/java/com/aurora/store/view/ui/sheets/AppMenuSheet.kt index 03f2978dc..e4f5fa1a6 100644 --- a/app/src/main/java/com/aurora/store/view/ui/sheets/AppMenuSheet.kt +++ b/app/src/main/java/com/aurora/store/view/ui/sheets/AppMenuSheet.kt @@ -107,8 +107,7 @@ class AppMenuSheet : BaseBottomSheet() { } R.id.action_uninstall -> { - AppInstaller - .with(requireContext()) + AppInstaller(requireContext()) .getPreferredInstaller().uninstall(app.packageName) } diff --git a/app/src/main/java/com/aurora/store/view/ui/sheets/BaseBottomSheet.kt b/app/src/main/java/com/aurora/store/view/ui/sheets/BaseBottomSheet.kt index a65fbb73c..c4f8c9539 100644 --- a/app/src/main/java/com/aurora/store/view/ui/sheets/BaseBottomSheet.kt +++ b/app/src/main/java/com/aurora/store/view/ui/sheets/BaseBottomSheet.kt @@ -38,7 +38,9 @@ abstract class BaseBottomSheet : BottomSheetDialogFragment() { lateinit var VM: SheetBaseBinding - var gson: Gson = GsonBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT).create() + var gson: Gson = GsonBuilder() + .excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT) + .create() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val bottomSheetDialog = BottomSheetDialog( diff --git a/app/src/main/java/com/aurora/store/view/ui/sheets/InstallErrorDialogSheet.kt b/app/src/main/java/com/aurora/store/view/ui/sheets/InstallErrorDialogSheet.kt new file mode 100644 index 000000000..b85443f85 --- /dev/null +++ b/app/src/main/java/com/aurora/store/view/ui/sheets/InstallErrorDialogSheet.kt @@ -0,0 +1,124 @@ +/* + * 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.sheets + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.aurora.Constants +import com.aurora.extensions.copyToClipBoard +import com.aurora.extensions.load +import com.aurora.extensions.px +import com.aurora.extensions.toast +import com.aurora.gplayapi.data.models.App +import com.aurora.store.R +import com.aurora.store.databinding.SheetInstallErrorBinding +import com.bumptech.glide.load.resource.bitmap.CircleCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners + +class InstallErrorDialogSheet : BaseBottomSheet() { + + private lateinit var B: SheetInstallErrorBinding + + private lateinit var app: App + private lateinit var title: String + private lateinit var error: String + private lateinit var extra: String + + private var rawApp = String() + + companion object { + private const val DIALOG_TITLE = "DIALOG_TITLE" + private const val DIALOG_ERROR = "DIALOG_ERROR" + private const val DIALOG_EXTRA = "DIALOG_EXTRA" + + @JvmStatic + fun newInstance( + app: App, + title: String?, + error: String?, + extra: String? + ): InstallErrorDialogSheet { + return InstallErrorDialogSheet().apply { + arguments = Bundle().apply { + putString(Constants.STRING_EXTRA, gson.toJson(app)) + putString(DIALOG_TITLE, title) + putString(DIALOG_ERROR, error) + putString(DIALOG_EXTRA, extra) + } + } + } + } + + override fun onCreateContentView( + inflater: LayoutInflater, + container: ViewGroup, + savedInstanceState: Bundle? + ): View { + B = SheetInstallErrorBinding.inflate(inflater, container, false) + + val bundle = arguments + bundle?.let { + rawApp = bundle.getString(Constants.STRING_EXTRA, "{}") + app = gson.fromJson(rawApp, App::class.java) + title = bundle.getString(DIALOG_TITLE, "") + error = bundle.getString(DIALOG_ERROR, "") + extra = bundle.getString(DIALOG_EXTRA, "") + + if (app.packageName.isNotEmpty()) { + inflateData() + } else { + dismissAllowingStateLoss() + } + } + + attachAction() + + return B.root + } + + override fun onContentViewCreated(view: View, savedInstanceState: Bundle?) { + + } + + private fun inflateData() { + B.imgIcon.load(app.iconArtwork.url) { + transform(CircleCrop()) + } + + B.txtLine1.text = app.displayName + B.txtLine2.text = error + B.txtLine3.text = extra + } + + private fun attachAction() { + B.btnPrimary.setOnClickListener { + dismissAllowingStateLoss() + } + + B.btnSecondary.setOnClickListener { + if (::extra.isInitialized) { + requireContext().copyToClipBoard(extra) + requireContext().toast(R.string.toast_clipboard_copied) + } + } + } +} diff --git a/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt b/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt index 817ccd534..e973f4043 100644 --- a/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt +++ b/app/src/main/java/com/aurora/store/view/ui/updates/UpdatesFragment.kt @@ -93,16 +93,22 @@ class UpdatesFragment : BaseFragment() { } } - fetch.addListener(fetchListener) return B.root } - override fun onDestroyView() { + override fun onResume() { + super.onResume() + if (::fetch.isInitialized && ::fetchListener.isInitialized) { + fetch.addListener(fetchListener) + } + } + + override fun onPause() { if (::fetch.isInitialized && ::fetchListener.isInitialized) { fetch.removeListener(fetchListener) } - super.onDestroyView() + super.onPause() } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -249,7 +255,7 @@ class UpdatesFragment : BaseFragment() { @Synchronized private fun install(packageName: String, files: List) { task { - AppInstaller.with(requireContext()) + AppInstaller(requireContext()) .getPreferredInstaller() .install( packageName, diff --git a/app/src/main/res/drawable/ic_install.xml b/app/src/main/res/drawable/ic_install.xml new file mode 100644 index 000000000..ee4cc10f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_install.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/app/src/main/res/layout-land/activity_account.xml b/app/src/main/res/layout-land/activity_account.xml index 46c7f0c15..438477d15 100644 --- a/app/src/main/res/layout-land/activity_account.xml +++ b/app/src/main/res/layout-land/activity_account.xml @@ -137,7 +137,7 @@ android:text="@string/account_login_using" android:textAlignment="center" /> - - - - - + android:layout_height="wrap_content" + android:animateFirstView="true" + android:inAnimation="@anim/fade_in" + android:outAnimation="@anim/fade_out" + tools:ignore="UselessParent"> - - - - - - - - + android:layout_height="wrap_content" + android:animateFirstView="true" + android:inAnimation="@anim/fade_in" + android:outAnimation="@anim/fade_out" + tools:ignore="UselessParent"> - - - + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_action_button.xml b/app/src/main/res/layout/view_action_button.xml index b4369a3c8..440ad3085 100644 --- a/app/src/main/res/layout/view_action_button.xml +++ b/app/src/main/res/layout/view_action_button.xml @@ -21,37 +21,59 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_height="@dimen/height_button"> - + android:layout_height="match_parent" + android:animateFirstView="true" + android:inAnimation="@anim/fade_in" + android:outAnimation="@anim/fade_out" + tools:ignore="UselessParent"> - + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_download_single.xml b/app/src/main/res/menu/menu_download_single.xml index 87b4484b4..9b8b52913 100644 --- a/app/src/main/res/menu/menu_download_single.xml +++ b/app/src/main/res/menu/menu_download_single.xml @@ -20,7 +20,7 @@ + android:title="@string/action_copy_link" /> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 1fc263af5..ab9c40d0f 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -38,6 +38,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31c8558d3..dfceb873e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,7 +56,9 @@ "Clear" "Clear all" "Clear data" - "Copy Link" + "Close" + "Copy" + "Copy Link" "Disable" "Discard" "Discover" @@ -92,6 +94,7 @@ "Manual download" "More" "Next" + "Okay" "Open" "Pause" "Pending" @@ -201,9 +204,12 @@ "Root access available" "No Root, grant root access or change the installer." + "Aurora services is available and ready to install." + "Aurora services not configured. Make sure service is enabled & all permissions are granted" "Installation failed" "Installation cancelled" "Installation was blocked" + "Could not create session" "Conflicting package exists" "Incompatible app" "Invalid or corrupted APK" @@ -290,6 +296,7 @@ "Games" "Installation" "Installed" + "App Installer" "Language" "Library" "Purchase history" diff --git a/app/src/main/res/values/styles_widget.xml b/app/src/main/res/values/styles_widget.xml index 6067c1ba8..a472bac16 100644 --- a/app/src/main/res/values/styles_widget.xml +++ b/app/src/main/res/values/styles_widget.xml @@ -72,4 +72,28 @@ + + + + + + \ No newline at end of file