Improve installer & related UI

This commit is contained in:
Rahul Kumar Patel
2021-02-25 08:57:30 +05:30
parent b877552634
commit 0e1ae6364d
34 changed files with 1379 additions and 158 deletions

View File

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

View File

@@ -36,6 +36,10 @@ class AuroraApplication : MultiDexApplication() {
private lateinit var fetch: Fetch
private lateinit var packageManagerReceiver: PackageManagerReceiver
companion object{
val enqueuedInstalls: MutableSet<String> = mutableSetOf()
}
override fun onCreate() {
super.onCreate()

View File

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

View File

@@ -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, Context>(::AppInstaller)
open class AppInstaller constructor(var context: Context) {
fun getPreferredInstaller(): IInstaller {
val prefValue = Preferences.getInteger(

View File

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

View File

@@ -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>(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"
}
}

View File

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

View File

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

View File

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

View File

@@ -43,7 +43,7 @@ open class PackageManagerReceiver : BroadcastReceiver() {
.post(InstallEvent(packageName, ""))
//Clear installation queue
AppInstaller.with(context)
AppInstaller(context)
.getPreferredInstaller()
.removeFromInstallQueue(packageName)
}

View File

@@ -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<Download>) {
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()
}
}

View File

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

View File

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

View File

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

View File

@@ -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<ValueAnimator> {
val animators = ArrayList<ValueAnimator>()
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
}
}

View File

@@ -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<ValueAnimator, ValueAnimator.AnimatorUpdateListener> updateListenerHashMap = new HashMap<>();
private final Paint paint = new Paint();
private int alpha = 255;
private ArrayList<ValueAnimator> 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<ValueAnimator> 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();
}
}

View File

@@ -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<Download>) {
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()
}
}
}

View File

@@ -107,8 +107,7 @@ class AppMenuSheet : BaseBottomSheet() {
}
R.id.action_uninstall -> {
AppInstaller
.with(requireContext())
AppInstaller(requireContext())
.getPreferredInstaller().uninstall(app.packageName)
}

View File

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

View File

@@ -0,0 +1,124 @@
/*
* 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.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)
}
}
}
}

View File

@@ -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<Download>) {
task {
AppInstaller.with(requireContext())
AppInstaller(requireContext())
.getPreferredInstaller()
.install(
packageName,