From 301e017df995e7aaa4876a6d67693dd9fb548dda Mon Sep 17 00:00:00 2001 From: huangjian Date: Tue, 1 Nov 2022 16:49:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EAppUpgrade=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 10 +- app/src/main/AndroidManifest.xml | 23 +- .../erban/upgrade/AppUpgradeHelper.java | 25 +- .../upgrade/base/BaseHttpDownloadManager.kt | 34 ++ .../erban/upgrade/base/bean/DownloadStatus.kt | 28 + .../upgrade/config/AppUpdateFileProvider.kt | 16 + .../erban/upgrade/config/Constant.kt | 59 +++ .../listener/LifecycleCallbacksAdapter.kt | 40 ++ .../upgrade/listener/OnButtonClickListener.kt | 28 + .../upgrade/listener/OnDownloadListener.kt | 43 ++ .../listener/OnDownloadListenerAdapter.kt | 31 ++ .../erban/upgrade/manager/DownloadManager.kt | 441 ++++++++++++++++ .../upgrade/manager/HttpDownloadManager.kt | 135 +++++ .../erban/upgrade/service/DownloadService.kt | 188 +++++++ .../tongdaxing/erban/upgrade/util/ApkUtil.kt | 82 +++ .../erban/upgrade/util/DensityUtil.kt | 23 + .../tongdaxing/erban/upgrade/util/FileUtil.kt | 47 ++ .../tongdaxing/erban/upgrade/util/LogUtil.kt | 39 ++ .../erban/upgrade/util/NotificationUtil.kt | 163 ++++++ .../erban/upgrade/view/NumberProgressBar.java | 479 ++++++++++++++++++ .../upgrade/view/UpdateDialogActivity.kt | 197 +++++++ .../res/drawable/bg_button.xml | 15 + .../res/drawable/bg_white_radius_6.xml | 7 + .../res/drawable/ic_dialog_close.png | Bin 0 -> 4434 bytes .../res/drawable/ic_dialog_default.png | Bin 0 -> 38961 bytes .../res/layout/dialog_update.xml | 97 ++++ .../module_upgrade_app/res/values/strings.xml | 12 + .../module_upgrade_app/res/values/styles.xml | 25 + .../res/xml/app_update_file.xml | 9 + gradle.properties | 4 +- 30 files changed, 2283 insertions(+), 17 deletions(-) create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/base/BaseHttpDownloadManager.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/base/bean/DownloadStatus.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/config/AppUpdateFileProvider.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/config/Constant.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/LifecycleCallbacksAdapter.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/OnButtonClickListener.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/OnDownloadListener.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/OnDownloadListenerAdapter.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/manager/DownloadManager.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/manager/HttpDownloadManager.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/service/DownloadService.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/ApkUtil.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/DensityUtil.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/FileUtil.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/LogUtil.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/NotificationUtil.kt create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/view/NumberProgressBar.java create mode 100644 app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/view/UpdateDialogActivity.kt create mode 100644 app/src/module_upgrade_app/res/drawable/bg_button.xml create mode 100644 app/src/module_upgrade_app/res/drawable/bg_white_radius_6.xml create mode 100644 app/src/module_upgrade_app/res/drawable/ic_dialog_close.png create mode 100644 app/src/module_upgrade_app/res/drawable/ic_dialog_default.png create mode 100644 app/src/module_upgrade_app/res/layout/dialog_update.xml create mode 100644 app/src/module_upgrade_app/res/values/styles.xml create mode 100644 app/src/module_upgrade_app/res/xml/app_update_file.xml diff --git a/app/build.gradle b/app/build.gradle index 1abf412b9..4ab6b7ff2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -175,8 +175,8 @@ android { def Lombok = "1.18.10" dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar','*.aar']) - implementation fileTree(dir: 'aliyun-libs', include: ['*.jar','*.aar']) + implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) + implementation fileTree(dir: 'aliyun-libs', include: ['*.jar', '*.aar']) testImplementation 'junit:junit:4.13.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' @@ -243,9 +243,9 @@ dependencies { implementation 'com.huawei.hms:push:6.5.0.300' //魅族推送 implementation 'com.meizu.flyme.internet:push-internal:4.1.0' - //oppo推送需要 + //oppo推送需要 implementation 'commons-codec:commons-codec:1.6' - + api 'com.tencent.vasdolly:helper:3.0.3' implementation "io.github.tencent:vap:2.0.24" @@ -254,7 +254,7 @@ dependencies { repositories { flatDir { - dirs 'aliyun-libs','com.huawei.agconnect' + dirs 'aliyun-libs', 'com.huawei.agconnect' } mavenCentral() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e34584635..a5c9d01f9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -270,8 +270,8 @@ + android:exported="true" + android:permission="com.push.permission.UPSTAGESERVICE" /> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/AppUpgradeHelper.java b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/AppUpgradeHelper.java index 7d465a769..c7afc822b 100644 --- a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/AppUpgradeHelper.java +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/AppUpgradeHelper.java @@ -2,13 +2,16 @@ package com.tongdaxing.erban.upgrade; import android.annotation.SuppressLint; -import com.trello.rxlifecycle3.android.ActivityEvent; -import com.trello.rxlifecycle3.components.support.RxAppCompatActivity; import com.mango.core.upgrade.bean.NewestVersionInfo; import com.mango.core.upgrade.bean.UpgradeCache; import com.mango.core.upgrade.model.UpgradeModel; import com.mango.core.utils.ActivityUtil; +import com.mango.moshen.R; +import com.mango.xchat_android_library.utils.JavaUtil; import com.mango.xchat_android_library.utils.SingleToastUtil; +import com.tongdaxing.erban.upgrade.manager.DownloadManager; +import com.trello.rxlifecycle3.android.ActivityEvent; +import com.trello.rxlifecycle3.components.support.RxAppCompatActivity; /** * @author jack @@ -18,9 +21,8 @@ import com.mango.xchat_android_library.utils.SingleToastUtil; public class AppUpgradeHelper { /** - * * @param isUserAuto ture 表示,是用户主动发起的请求,比如设置页点更新 - * @param isPush ture 表示是后台推送 + * @param isPush ture 表示是后台推送 */ @SuppressLint("CheckResult") public static void checkAppUpgrade(RxAppCompatActivity activity, boolean isUserAuto, @@ -73,10 +75,17 @@ public class AppUpgradeHelper { } //如果是强更,一定要弹窗 if (forceUpdate || needShow) { - AppUpdateDialog appUpdateDialog = new AppUpdateDialog(); - appUpdateDialog.setNewestVersionInfo(newestVersionInfo); - appUpdateDialog.show(activity.getSupportFragmentManager()); - UpgradeModel.get().setHasShowDialog(true); + DownloadManager manager = new DownloadManager.Builder(activity) + .apkUrl(newestVersionInfo.getUpdateDownloadLink()) + .apkName("magic_v" + newestVersionInfo.getUpdateVersion() + ".apk") + .apkMD5(newestVersionInfo.getUpdateFileMd5()) + .apkVersionName(newestVersionInfo.getUpdateVersion()) + .smallIcon(R.mipmap.app_logo) + .forcedUpgrade(forceUpdate) + .apkVersionCode(Integer.MAX_VALUE) + .apkDescription(newestVersionInfo.getUpdateVersionDesc()) + .build(); + manager.download(); } } } else { diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/base/BaseHttpDownloadManager.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/base/BaseHttpDownloadManager.kt new file mode 100644 index 000000000..1d58583b2 --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/base/BaseHttpDownloadManager.kt @@ -0,0 +1,34 @@ +package com.tongdaxing.erban.upgrade.base + +import com.tongdaxing.erban.upgrade.base.bean.DownloadStatus +import kotlinx.coroutines.flow.Flow + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.base + * FileName: BaseHttpDownloadManager + * CreateDate: 2022/4/7 on 10:24 + * Desc: + * + * @author azhon + */ + +abstract class BaseHttpDownloadManager { + /** + * download apk from apkUrl + * + * @param apkUrl + * @param apkName + */ + abstract fun download(apkUrl: String, apkName: String): Flow + + /** + * cancel download apk + */ + abstract fun cancel() + + /** + * release memory + */ + abstract fun release() +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/base/bean/DownloadStatus.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/base/bean/DownloadStatus.kt new file mode 100644 index 000000000..524f5abff --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/base/bean/DownloadStatus.kt @@ -0,0 +1,28 @@ +package com.tongdaxing.erban.upgrade.base.bean + +import java.io.File + + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.base.bean + * FileName: DownloadStatus + * CreateDate: 2022/4/14 on 11:18 + * Desc: + * + * @author azhon + */ + + +sealed class DownloadStatus { + + object Start : DownloadStatus() + + data class Downloading(val max: Int, val progress: Int) : DownloadStatus() + + class Done(val apk: File) : DownloadStatus() + + object Cancel : DownloadStatus() + + data class Error(val e: Throwable) : DownloadStatus() +} diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/config/AppUpdateFileProvider.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/config/AppUpdateFileProvider.kt new file mode 100644 index 000000000..0a173ee5e --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/config/AppUpdateFileProvider.kt @@ -0,0 +1,16 @@ +package com.tongdaxing.erban.upgrade.config + +import androidx.core.content.FileProvider + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.config + * FileName: AppUpdateFileProvider + * CreateDate: 2022/4/7 on 10:30 + * Desc: + * + * @author azhon + */ + + +class AppUpdateFileProvider : FileProvider() \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/config/Constant.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/config/Constant.kt new file mode 100644 index 000000000..426b63f59 --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/config/Constant.kt @@ -0,0 +1,59 @@ +package com.tongdaxing.erban.upgrade.config + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.config + * FileName: Constant + * CreateDate: 2022/4/7 on 10:28 + * Desc: + * + * @author azhon + */ + +object Constant { + + /** + * Http timeout(ms) + */ + const val HTTP_TIME_OUT = 30_000 + + /** + * Logcat tag + */ + const val TAG = "AppUpdate." + + /** + * Apk file extension + */ + const val APK_SUFFIX = ".apk" + + /** + * Coroutine Name + */ + const val COROUTINE_NAME = "app-update-coroutine" + + /** + * Notification channel id + */ + const val DEFAULT_CHANNEL_ID = "appUpdate" + + /** + * Notification id + */ + const val DEFAULT_NOTIFY_ID = 1011 + + /** + * Notification channel name + */ + const val DEFAULT_CHANNEL_NAME = "AppUpdate" + + /** + * Compat Android N file uri + */ + var AUTHORITIES: String? = null + + /** + * Apk path + */ + var APK_PATH = "/storage/emulated/0/Android/data/%s/cache" +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/LifecycleCallbacksAdapter.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/LifecycleCallbacksAdapter.kt new file mode 100644 index 000000000..e897b2ef8 --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/LifecycleCallbacksAdapter.kt @@ -0,0 +1,40 @@ +package com.tongdaxing.erban.upgrade.listener + +import android.app.Activity +import android.app.Application +import android.os.Bundle + + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.listener + * FileName: LifecycleCallbacksAdapter + * CreateDate: 2022/4/8 on 11:26 + * Desc: + * + * @author azhon + */ + +abstract class LifecycleCallbacksAdapter : Application.ActivityLifecycleCallbacks { + + override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + } + + override fun onActivityStarted(activity: Activity) { + } + + override fun onActivityResumed(activity: Activity) { + } + + override fun onActivityPaused(activity: Activity) { + } + + override fun onActivityStopped(activity: Activity) { + } + + override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + } + + override fun onActivityDestroyed(activity: Activity) { + } +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/OnButtonClickListener.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/OnButtonClickListener.kt new file mode 100644 index 000000000..95283d9a0 --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/OnButtonClickListener.kt @@ -0,0 +1,28 @@ +package com.tongdaxing.erban.upgrade.listener + + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.listener + * FileName: OnButtonClickListener + * CreateDate: 2022/4/7 on 15:56 + * Desc: + * + * @author azhon + */ + +interface OnButtonClickListener { + companion object { + /** + * click update button + */ + const val UPDATE = 0 + + /** + * click cancel button + */ + const val CANCEL = 1 + } + + fun onButtonClick(id: Int) +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/OnDownloadListener.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/OnDownloadListener.kt new file mode 100644 index 000000000..94a245210 --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/OnDownloadListener.kt @@ -0,0 +1,43 @@ +package com.tongdaxing.erban.upgrade.listener + +import java.io.File + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.listener + * FileName: OnDownloadListener + * CreateDate: 2022/4/7 on 10:27 + * Desc: + * + * @author azhon + */ + +interface OnDownloadListener { + /** + * start download + */ + fun start() + + /** + * + * @param max file length + * @param progress downloaded file size + */ + fun downloading(max: Int, progress: Int) + + /** + * @param apk + */ + fun done(apk: File) + + /** + * cancel download + */ + fun cancel() + + /** + * + * @param e + */ + fun error(e: Throwable) +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/OnDownloadListenerAdapter.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/OnDownloadListenerAdapter.kt new file mode 100644 index 000000000..34eaf6c09 --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/listener/OnDownloadListenerAdapter.kt @@ -0,0 +1,31 @@ +package com.tongdaxing.erban.upgrade.listener + +import java.io.File + + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.listener + * FileName: OnDownloadListenerAdapter + * CreateDate: 2022/4/8 on 10:58 + * Desc: + * + * @author azhon + */ + +abstract class OnDownloadListenerAdapter : OnDownloadListener { + override fun start() { + } + + override fun downloading(max: Int, progress: Int) { + } + + override fun done(apk: File) { + } + + override fun cancel() { + } + + override fun error(e: Throwable) { + } +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/manager/DownloadManager.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/manager/DownloadManager.kt new file mode 100644 index 000000000..eb6ebcca5 --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/manager/DownloadManager.kt @@ -0,0 +1,441 @@ +package com.tongdaxing.erban.upgrade.manager + +import android.app.Activity +import android.app.Application +import android.app.NotificationChannel +import android.content.Intent +import android.widget.Toast +import com.mango.moshen.R +import com.tongdaxing.erban.upgrade.base.BaseHttpDownloadManager +import com.tongdaxing.erban.upgrade.config.Constant +import com.tongdaxing.erban.upgrade.listener.LifecycleCallbacksAdapter +import com.tongdaxing.erban.upgrade.listener.OnButtonClickListener +import com.tongdaxing.erban.upgrade.listener.OnDownloadListener +import com.tongdaxing.erban.upgrade.service.DownloadService +import com.tongdaxing.erban.upgrade.util.ApkUtil +import com.tongdaxing.erban.upgrade.util.LogUtil +import com.tongdaxing.erban.upgrade.view.UpdateDialogActivity +import java.io.Serializable + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.manager + * FileName: DownloadManager + * CreateDate: 2022/4/7 on 10:36 + * Desc: + * + * @author azhon + */ +class DownloadManager private constructor(builder: Builder) : Serializable { + + companion object { + private const val TAG = "DownloadManager" + private var instance: DownloadManager? = null + + fun getInstance(builder: Builder? = null): DownloadManager? { + if (instance != null && builder != null) { + instance!!.release() + } + if (instance == null) { + if (builder == null) return null + instance = DownloadManager(builder) + } + return instance!! + } + } + + var application: Application = builder.application + var contextClsName: String = builder.contextClsName + var downloadState: Boolean = false + var apkUrl: String + var apkName: String + var apkVersionCode: Int + var apkVersionName: String + var downloadPath: String + var showNewerToast: Boolean + var smallIcon: Int + var apkDescription: String + var apkSize: String + var apkMD5: String + var httpManager: BaseHttpDownloadManager? + var notificationChannel: NotificationChannel? + var onDownloadListeners: MutableList + var onButtonClickListener: OnButtonClickListener? + var showNotification: Boolean + var jumpInstallPage: Boolean + var showBgdToast: Boolean + var forcedUpgrade: Boolean + var notifyId: Int + var dialogImage: Int + var dialogButtonColor: Int + var dialogButtonTextColor: Int + var dialogProgressBarColor: Int + + + init { + apkUrl = builder.apkUrl + apkName = builder.apkName + apkVersionCode = builder.apkVersionCode + apkVersionName = builder.apkVersionName + downloadPath = + builder.downloadPath ?: String.format(Constant.APK_PATH, application.packageName) + showNewerToast = builder.showNewerToast + smallIcon = builder.smallIcon + apkDescription = builder.apkDescription + apkSize = builder.apkSize + apkMD5 = builder.apkMD5 + httpManager = builder.httpManager + notificationChannel = builder.notificationChannel + onDownloadListeners = builder.onDownloadListeners + onButtonClickListener = builder.onButtonClickListener + showNotification = builder.showNotification + jumpInstallPage = builder.jumpInstallPage + showBgdToast = builder.showBgdToast + forcedUpgrade = builder.forcedUpgrade + notifyId = builder.notifyId + dialogImage = builder.dialogImage + dialogButtonColor = builder.dialogButtonColor + dialogButtonTextColor = builder.dialogButtonTextColor + dialogProgressBarColor = builder.dialogProgressBarColor + // Fix memory leak + application.registerActivityLifecycleCallbacks(object : LifecycleCallbacksAdapter() { + override fun onActivityDestroyed(activity: Activity) { + super.onActivityDestroyed(activity) + if (contextClsName == activity.javaClass.name) { + clearListener() + } + } + }) + } + + /** + * Start download + */ + fun download() { + if (!checkParams()) { + return + } + if (checkVersionCode()) { + application.startService(Intent(application, DownloadService::class.java)) + } else { + if (apkVersionCode > ApkUtil.getVersionCode(application)) { + application.startActivity( + Intent(application, UpdateDialogActivity::class.java) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + ) + } else { + if (showNewerToast) { + Toast.makeText(application, R.string.latest_version, Toast.LENGTH_SHORT).show() + } + LogUtil.d(TAG, application.resources.getString(R.string.latest_version)) + } + } + + } + + private fun checkParams(): Boolean { + if (apkUrl.isEmpty()) { + LogUtil.e(TAG, "apkUrl can not be empty!") + return false + } + if (apkName.isEmpty()) { + LogUtil.e(TAG, "apkName can not be empty!") + return false + } + if (!apkName.endsWith(Constant.APK_SUFFIX)) { + LogUtil.e(TAG, "apkName must endsWith .apk!") + return false + } + if (smallIcon == -1) { + LogUtil.e(TAG, "smallIcon can not be empty!"); + return false + } + Constant.AUTHORITIES = "${application.packageName}.fileProvider" + return true + } + + /** + * Check the set apkVersionCode if it is not the default then use the built-in dialog + * If it is the default value Int.MIN_VALUE, directly start the service download + */ + private fun checkVersionCode(): Boolean { + if (apkVersionCode == Int.MIN_VALUE) { + return true + } + if (apkDescription.isEmpty()) { + LogUtil.e(TAG, "apkDescription can not be empty!") + } + return false + } + + fun cancel() { + httpManager?.cancel() + } + + /** + * release objects + */ + internal fun release() { + httpManager?.release() + clearListener() + instance = null + } + + private fun clearListener() { + onButtonClickListener = null + onDownloadListeners.clear() + } + + class Builder constructor(activity: Activity) { + + /** + * library context + */ + internal var application: Application = activity.application + + /** + * Fix the memory leak caused by Activity destroy + */ + internal var contextClsName: String = activity.javaClass.name + + /** + * Apk download url + */ + internal var apkUrl = "" + + /** + * Apk file name on disk + */ + internal var apkName = "" + + /** + * The apk versionCode that needs to be downloaded + */ + internal var apkVersionCode = Int.MIN_VALUE + + /** + * The versionName of the dialog reality + */ + internal var apkVersionName = "" + + /** + * The file path where the Apk is saved + * eg: /storage/emulated/0/Android/data/ your packageName /cache + */ + internal var downloadPath = application.externalCacheDir?.path + + /** + * whether to tip to user "Currently the latest version!" + */ + internal var showNewerToast = false + + /** + * Notification icon resource + */ + internal var smallIcon = -1 + + /** + * New version description information + */ + internal var apkDescription = "" + + /** + * Apk Size,Unit MB + */ + internal var apkSize = "" + + /** + * Apk md5 file verification(32-bit) verification repeated download + */ + internal var apkMD5 = "" + + /** + * Apk download manager + */ + internal var httpManager: BaseHttpDownloadManager? = null + + /** + * The following are unimportant filed + */ + + /** + * adapter above Android O notification + */ + internal var notificationChannel: NotificationChannel? = null + + /** + * download listeners + */ + internal var onDownloadListeners = mutableListOf() + + /** + * dialog button click listener + */ + internal var onButtonClickListener: OnButtonClickListener? = null + + /** + * Whether to show the progress of the notification + */ + internal var showNotification = true + + /** + * Whether the installation page will pop up automatically after the download is complete + */ + internal var jumpInstallPage = true + + /** + * Does the download start tip "Downloading a new version in the background..." + */ + internal var showBgdToast = true + + /** + * Whether to force an upgrade + */ + internal var forcedUpgrade = false + + /** + * Notification id + */ + internal var notifyId = Constant.DEFAULT_NOTIFY_ID + + /** + * dialog background Image resource + */ + internal var dialogImage = -1 + + /** + * dialog button background color + */ + internal var dialogButtonColor = -1 + + /** + * dialog button text color + */ + internal var dialogButtonTextColor = -1 + + /** + * dialog progress bar color and progress-text color + */ + internal var dialogProgressBarColor = -1 + + + fun apkUrl(apkUrl: String): Builder { + this.apkUrl = apkUrl + return this + } + + fun apkName(apkName: String): Builder { + this.apkName = apkName + return this + } + + fun apkVersionCode(apkVersionCode: Int): Builder { + this.apkVersionCode = apkVersionCode + return this + } + + fun apkVersionName(apkVersionName: String): Builder { + this.apkVersionName = apkVersionName + return this + } + + fun showNewerToast(showNewerToast: Boolean): Builder { + this.showNewerToast = showNewerToast + return this + } + + fun smallIcon(smallIcon: Int): Builder { + this.smallIcon = smallIcon + return this + } + + fun apkDescription(apkDescription: String): Builder { + this.apkDescription = apkDescription + return this + } + + fun apkSize(apkSize: String): Builder { + this.apkSize = apkSize + return this + } + + fun apkMD5(apkMD5: String): Builder { + this.apkMD5 = apkMD5 + return this + } + + fun httpManager(httpManager: BaseHttpDownloadManager): Builder { + this.httpManager = httpManager + return this + } + + fun notificationChannel(notificationChannel: NotificationChannel): Builder { + this.notificationChannel = notificationChannel + return this + } + + fun onButtonClickListener(onButtonClickListener: OnButtonClickListener): Builder { + this.onButtonClickListener = onButtonClickListener + return this + } + + fun onDownloadListener(onDownloadListener: OnDownloadListener): Builder { + this.onDownloadListeners.add(onDownloadListener) + return this + } + + fun showNotification(showNotification: Boolean): Builder { + this.showNotification = showNotification + return this + } + + fun jumpInstallPage(jumpInstallPage: Boolean): Builder { + this.jumpInstallPage = jumpInstallPage + return this + } + + fun showBgdToast(showBgdToast: Boolean): Builder { + this.showBgdToast = showBgdToast + return this + } + + fun forcedUpgrade(forcedUpgrade: Boolean): Builder { + this.forcedUpgrade = forcedUpgrade + return this + } + + fun notifyId(notifyId: Int): Builder { + this.notifyId = notifyId + return this + } + + fun dialogImage(dialogImage: Int): Builder { + this.dialogImage = dialogImage + return this + } + + fun dialogButtonColor(dialogButtonColor: Int): Builder { + this.dialogButtonColor = dialogButtonColor + return this + } + + fun dialogButtonTextColor(dialogButtonTextColor: Int): Builder { + this.dialogButtonTextColor = dialogButtonTextColor + return this + } + + fun dialogProgressBarColor(dialogProgressBarColor: Int): Builder { + this.dialogProgressBarColor = dialogProgressBarColor + return this + } + + fun enableLog(enable: Boolean): Builder { + LogUtil.enable(enable) + return this + } + + fun build(): DownloadManager { + return getInstance(this)!! + } + } +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/manager/HttpDownloadManager.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/manager/HttpDownloadManager.kt new file mode 100644 index 000000000..b3bc754cd --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/manager/HttpDownloadManager.kt @@ -0,0 +1,135 @@ +package com.tongdaxing.erban.upgrade.manager + +import com.tongdaxing.erban.upgrade.base.BaseHttpDownloadManager +import com.tongdaxing.erban.upgrade.base.bean.DownloadStatus +import com.tongdaxing.erban.upgrade.config.Constant +import com.tongdaxing.erban.upgrade.util.LogUtil +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import java.io.File +import java.io.FileOutputStream +import java.net.HttpURLConnection +import java.net.SocketTimeoutException +import java.net.URL +import java.security.SecureRandom +import java.security.cert.X509Certificate +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLContext +import javax.net.ssl.TrustManager +import javax.net.ssl.X509TrustManager + + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.manager + * FileName: HttpDownloadManager + * CreateDate: 2022/4/7 on 14:29 + * Desc: + * + * @author azhon + */ + +@Suppress("BlockingMethodInNonBlockingContext") +class HttpDownloadManager(private val path: String) : BaseHttpDownloadManager() { + companion object { + private const val TAG = "HttpDownloadManager" + } + + private var shutdown: Boolean = false + + override fun download(apkUrl: String, apkName: String): Flow { + trustAllHosts() + shutdown = false + File(path, apkName).let { + if (it.exists()) it.delete() + } + return flow { + emit(DownloadStatus.Start) + connectToDownload(apkUrl, apkName, this) + }.catch { + emit(DownloadStatus.Error(it)) + }.flowOn(Dispatchers.IO) + } + + private suspend fun connectToDownload( + apkUrl: String, apkName: String, flow: FlowCollector + ) { + val con = URL(apkUrl).openConnection() as HttpURLConnection + con.apply { + requestMethod = "GET" + readTimeout = Constant.HTTP_TIME_OUT + connectTimeout = Constant.HTTP_TIME_OUT + setRequestProperty("Accept-Encoding", "identity") + } + if (con.responseCode == HttpURLConnection.HTTP_OK) { + val inStream = con.inputStream + val length = con.contentLength + var len: Int + var progress = 0 + val buffer = ByteArray(1024 * 2) + val file = File(path, apkName) + FileOutputStream(file).use { out -> + while (inStream.read(buffer).also { len = it } != -1 && !shutdown) { + out.write(buffer, 0, len) + progress += len + flow.emit(DownloadStatus.Downloading(length, progress)) + } + out.flush() + } + inStream.close() + if (shutdown) { + flow.emit(DownloadStatus.Cancel) + } else { + flow.emit(DownloadStatus.Done(file)) + } + } else if (con.responseCode == HttpURLConnection.HTTP_MOVED_PERM + || con.responseCode == HttpURLConnection.HTTP_MOVED_TEMP + ) { + con.disconnect() + val locationUrl = con.getHeaderField("Location") + LogUtil.d( + TAG, + "The current url is the redirect Url, the redirected url is $locationUrl" + ) + connectToDownload(locationUrl, apkName, flow) + } else { + val e = SocketTimeoutException("Error: Http response code = ${con.responseCode}") + flow.emit(DownloadStatus.Error(e)) + } + con.disconnect() + } + + /** + * fix https url (SSLHandshakeException) exception + */ + private fun trustAllHosts() { + val manager: TrustManager = object : X509TrustManager { + override fun getAcceptedIssuers(): Array { + return arrayOf() + } + + override fun checkClientTrusted(chain: Array?, authType: String?) { + LogUtil.d(TAG, "checkClientTrusted") + } + + override fun checkServerTrusted(chain: Array?, authType: String?) { + LogUtil.d(TAG, "checkServerTrusted") + } + } + try { + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, arrayOf(manager), SecureRandom()) + HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.socketFactory) + } catch (e: Exception) { + LogUtil.e(TAG, "trustAllHosts error: $e") + } + } + + override fun cancel() { + shutdown = true + } + + override fun release() { + cancel() + } +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/service/DownloadService.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/service/DownloadService.kt new file mode 100644 index 000000000..5546f78fc --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/service/DownloadService.kt @@ -0,0 +1,188 @@ +package com.tongdaxing.erban.upgrade.service + +import android.app.Service +import android.content.Intent +import android.os.Build +import android.os.IBinder +import android.widget.Toast +import com.mango.moshen.R +import com.tongdaxing.erban.upgrade.base.bean.DownloadStatus +import com.tongdaxing.erban.upgrade.config.Constant +import com.tongdaxing.erban.upgrade.listener.OnDownloadListener +import com.tongdaxing.erban.upgrade.manager.DownloadManager +import com.tongdaxing.erban.upgrade.manager.HttpDownloadManager +import com.tongdaxing.erban.upgrade.util.ApkUtil +import com.tongdaxing.erban.upgrade.util.FileUtil +import com.tongdaxing.erban.upgrade.util.LogUtil +import com.tongdaxing.erban.upgrade.util.NotificationUtil +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.collect +import java.io.File + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.service + * FileName: DownloadService + * CreateDate: 2022/4/7 on 11:42 + * Desc: + * + * @author azhon + */ + +class DownloadService : Service(), OnDownloadListener { + companion object { + private const val TAG = "DownloadService" + } + + private lateinit var manager: DownloadManager + private var lastProgress = 0 + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent == null) { + return START_NOT_STICKY + } + init() + return super.onStartCommand(intent, flags, startId) + } + + private fun init() { + val tempManager = DownloadManager.getInstance() + if (tempManager == null) { + LogUtil.e(TAG, "An exception occurred by DownloadManager=null,please check your code!") + return + } + manager = tempManager + FileUtil.createDirDirectory(manager.downloadPath) + + val enable = NotificationUtil.notificationEnable(this@DownloadService) + LogUtil.d( + TAG, + if (enable) "Notification switch status: opened" else " Notification switch status: closed" + ) + if (checkApkMd5()) { + LogUtil.d(TAG, "Apk already exist and install it directly.") + //install apk + done(File(manager.downloadPath, manager.apkName)) + } else { + LogUtil.d(TAG, "Apk don't exist will start download.") + download() + } + } + + /** + * Check whether the Apk has been downloaded, don't download again + */ + private fun checkApkMd5(): Boolean { + val file = File(manager.downloadPath, manager.apkName) + if (file.exists()) { + return FileUtil.md5(file).equals(manager.apkMD5, ignoreCase = true) + } + return false + } + + @Synchronized + private fun download() { + if (manager.downloadState) { + LogUtil.e(TAG, "Currently downloading, please download again!") + return + } + if (manager.httpManager == null) { + manager.httpManager = HttpDownloadManager(manager.downloadPath) + } + GlobalScope.launch(Dispatchers.Main + CoroutineName(Constant.COROUTINE_NAME)) { + manager.httpManager!!.download(manager.apkUrl, manager.apkName) + .collect { + when (it) { + is DownloadStatus.Start -> start() + is DownloadStatus.Downloading -> downloading(it.max, it.progress) + is DownloadStatus.Done -> done(it.apk) + is DownloadStatus.Cancel -> this@DownloadService.cancel() + is DownloadStatus.Error -> error(it.e) + } + } + } + manager.downloadState = true + } + + override fun start() { + LogUtil.i(TAG, "download start") + if (manager.showBgdToast) { + Toast.makeText(this, R.string.background_downloading, Toast.LENGTH_SHORT).show() + } + if (manager.showNotification) { + NotificationUtil.showNotification( + this@DownloadService, manager.smallIcon, + resources.getString(R.string.start_download), + resources.getString(R.string.start_download_hint) + ) + } + manager.onDownloadListeners.forEach { it.start() } + } + + override fun downloading(max: Int, progress: Int) { + if (manager.showNotification) { + val curr = (progress / max.toDouble() * 100.0).toInt() + if (curr == lastProgress) return + LogUtil.i(TAG, "downloading max: $max --- progress: $progress") + lastProgress = curr + val content = if (curr < 0) "" else "$curr%" + NotificationUtil.showProgressNotification( + this@DownloadService, manager.smallIcon, + resources.getString(R.string.start_downloading), + content, if (max == -1) -1 else 100, curr + ) + } + manager.onDownloadListeners.forEach { it.downloading(max, progress) } + } + + override fun done(apk: File) { + LogUtil.d(TAG, "apk downloaded to ${apk.path}") + manager.downloadState = false + //If it is android Q (api=29) and above, (showNotification=false) will also send a + // download completion notification + if (manager.showNotification || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + NotificationUtil.showDoneNotification( + this@DownloadService, manager.smallIcon, + resources.getString(R.string.download_completed), + resources.getString(R.string.click_hint), + Constant.AUTHORITIES!!, apk + ) + } + if (manager.jumpInstallPage) { + ApkUtil.installApk(this@DownloadService, Constant.AUTHORITIES!!, apk) + } + manager.onDownloadListeners.forEach { it.done(apk) } + + // release objects + releaseResources() + } + + override fun cancel() { + LogUtil.i(TAG, "download cancel") + manager.downloadState = false + if (manager.showNotification) { + NotificationUtil.cancelNotification(this@DownloadService) + } + manager.onDownloadListeners.forEach { it.cancel() } + } + + override fun error(e: Throwable) { + LogUtil.e(TAG, "download error: $e") + manager.downloadState = false + if (manager.showNotification) { + NotificationUtil.showErrorNotification( + this@DownloadService, manager.smallIcon, + resources.getString(R.string.download_error), + resources.getString(R.string.continue_downloading), + ) + } + manager.onDownloadListeners.forEach { it.error(e) } + } + + private fun releaseResources() { + manager.release() + stopSelf() + } +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/ApkUtil.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/ApkUtil.kt new file mode 100644 index 000000000..15c6e0450 --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/ApkUtil.kt @@ -0,0 +1,82 @@ +package com.tongdaxing.erban.upgrade.util + +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import androidx.core.content.FileProvider +import java.io.File + + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.util + * FileName: ApkUtil + * CreateDate: 2022/4/7 on 17:02 + * Desc: + * + * @author azhon + */ + +class ApkUtil { + companion object { + /** + * install package form file + */ + fun installApk(context: Context, authorities: String, apk: File) { + context.startActivity(createInstallIntent(context, authorities, apk)) + } + + fun createInstallIntent(context: Context, authorities: String, apk: File): Intent { + val intent = Intent().apply { + action = Intent.ACTION_VIEW + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addCategory(Intent.CATEGORY_DEFAULT) + } + val uri: Uri + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + uri = FileProvider.getUriForFile(context, authorities, apk) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } else { + uri = Uri.fromFile(apk) + } + intent.setDataAndType(uri, "application/vnd.android.package-archive") + return intent + } + + fun getVersionCode(context: Context): Long { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo.longVersionCode + } else { + return packageInfo.versionCode.toLong() + } + } + + fun deleteOldApk(context: Context, oldApkPath: String): Boolean { + val curVersionCode = getVersionCode(context) + try { + val apk = File(oldApkPath) + if (apk.exists()) { + val oldVersionCode = getVersionCodeByPath(context, oldApkPath) + if (curVersionCode > oldVersionCode) { + return apk.delete() + } + } + } catch (e: Exception) { + } + return false + } + + private fun getVersionCodeByPath(context: Context, path: String): Long { + val packageInfo = + context.packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo?.longVersionCode ?: 1 + } else { + return packageInfo?.versionCode?.toLong() ?: 1 + } + } + } +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/DensityUtil.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/DensityUtil.kt new file mode 100644 index 000000000..7edc5b07e --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/DensityUtil.kt @@ -0,0 +1,23 @@ +package com.tongdaxing.erban.upgrade.util + +import android.content.Context + + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.util + * FileName: DensityUtil + * CreateDate: 2022/4/7 on 17:52 + * Desc: + * + * @author azhon + */ + +class DensityUtil { + companion object { + fun dip2px(context: Context, dpValue: Float): Float { + val scale = context.resources.displayMetrics.density + return dpValue * scale + 0.5f + } + } +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/FileUtil.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/FileUtil.kt new file mode 100644 index 000000000..c16d3f5ea --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/FileUtil.kt @@ -0,0 +1,47 @@ +package com.tongdaxing.erban.upgrade.util + +import java.io.File +import java.io.FileInputStream +import java.math.BigInteger +import java.security.MessageDigest +import java.util.* + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.util + * FileName: FileUtil + * CreateDate: 2022/4/7 on 11:52 + * Desc: + * + * @author azhon + */ + +class FileUtil { + companion object { + fun createDirDirectory(path: String) { + File(path).let { + if (!it.exists()) { + it.mkdirs() + } + } + } + + fun md5(file: File): String { + try { + val buffer = ByteArray(1024) + var len: Int + val digest = MessageDigest.getInstance("MD5") + val inStream = FileInputStream(file) + while (inStream.read(buffer).also { len = it } != -1) { + digest.update(buffer, 0, len) + } + inStream.close() + val bigInt = BigInteger(1, digest.digest()) + return bigInt.toString(16).toUpperCase(Locale.ROOT) + } catch (e: Exception) { + e.printStackTrace() + } + return "" + } + } +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/LogUtil.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/LogUtil.kt new file mode 100644 index 000000000..32e20d4e8 --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/LogUtil.kt @@ -0,0 +1,39 @@ +package com.tongdaxing.erban.upgrade.util + +import android.util.Log +import com.tongdaxing.erban.upgrade.config.Constant + + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.util + * FileName: LogUtil + * CreateDate: 2022/4/7 on 11:23 + * Desc: + * + * @author azhon + */ + +class LogUtil { + + companion object { + var b = true + + fun enable(enable: Boolean) { + b = enable + } + + fun e(tag: String, msg: String) { + if (b) Log.e(Constant.TAG + tag, msg) + } + + fun d(tag: String, msg: String) { + if (b) Log.d(Constant.TAG + tag, msg) + } + + fun i(tag: String, msg: String) { + if (b) Log.i(Constant.TAG + tag, msg) + } + + } +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/NotificationUtil.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/NotificationUtil.kt new file mode 100644 index 000000000..7f65c8dda --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/util/NotificationUtil.kt @@ -0,0 +1,163 @@ +package com.tongdaxing.erban.upgrade.util + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.tongdaxing.erban.upgrade.config.Constant +import com.tongdaxing.erban.upgrade.manager.DownloadManager +import com.tongdaxing.erban.upgrade.service.DownloadService +import java.io.File + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.util + * FileName: NotificationUtil + * CreateDate: 2022/4/7 on 13:36 + * Desc: + * + * @author azhon + */ +class NotificationUtil { + companion object { + + fun notificationEnable(context: Context): Boolean { + return NotificationManagerCompat.from(context).areNotificationsEnabled() + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private fun getNotificationChannelId(): String { + val channel = DownloadManager.getInstance()?.notificationChannel + return if (channel == null) { + Constant.DEFAULT_CHANNEL_ID + } else { + channel.id + } + } + + private fun builderNotification( + context: Context, icon: Int, title: String, content: String + ): NotificationCompat.Builder { + var channelId = "" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + channelId = getNotificationChannelId() + } + return NotificationCompat.Builder(context, channelId) + .setSmallIcon(icon) + .setContentTitle(title) + .setWhen(System.currentTimeMillis()) + .setContentText(content) + .setAutoCancel(false) + .setOngoing(true) + } + + fun showNotification(context: Context, icon: Int, title: String, content: String) { + val manager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + afterO(manager) + } + val notify = builderNotification(context, icon, title, content) + .setDefaults(Notification.DEFAULT_SOUND) + .build() + manager.notify( + DownloadManager.getInstance()?.notifyId ?: Constant.DEFAULT_NOTIFY_ID, + notify + ) + } + + /** + * send a downloading Notification + */ + fun showProgressNotification( + context: Context, icon: Int, title: String, content: String, max: Int, progress: Int + ) { + val manager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val notify = builderNotification(context, icon, title, content) + .setProgress(max, progress, max == -1).build() + manager.notify( + DownloadManager.getInstance()?.notifyId ?: Constant.DEFAULT_NOTIFY_ID, + notify + ) + } + + /** + * send a downloaded Notification + */ + fun showDoneNotification( + context: Context, icon: Int, title: String, content: String, + authorities: String, apk: File + ) { + val manager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + manager.cancel(DownloadManager.getInstance()?.notifyId ?: Constant.DEFAULT_NOTIFY_ID) + val intent = ApkUtil.createInstallIntent(context, authorities, apk) + val pi = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val notify = builderNotification(context, icon, title, content) + .setContentIntent(pi) + .build() + notify.flags = notify.flags or Notification.FLAG_AUTO_CANCEL + manager.notify( + DownloadManager.getInstance()?.notifyId ?: Constant.DEFAULT_NOTIFY_ID, + notify + ) + } + + /** + * send a error Notification + */ + fun showErrorNotification( + context: Context, icon: Int, title: String, content: String + ) { + val manager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + afterO(manager) + } + val intent = Intent(context, DownloadService::class.java) + val pi = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + val notify = builderNotification(context, icon, title, content) + .setAutoCancel(true) + .setOngoing(false) + .setContentIntent(pi) + .setDefaults(Notification.DEFAULT_SOUND) + .build() + manager.notify( + DownloadManager.getInstance()?.notifyId ?: Constant.DEFAULT_NOTIFY_ID, + notify + ) + } + + /** + * cancel Notification by id + */ + fun cancelNotification(context: Context) { + val manager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + manager.cancel(DownloadManager.getInstance()?.notifyId ?: Constant.DEFAULT_NOTIFY_ID) + + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private fun afterO(manager: NotificationManager) { + var channel = DownloadManager.getInstance()?.notificationChannel + if (channel == null) { + channel = NotificationChannel( + Constant.DEFAULT_CHANNEL_ID, Constant.DEFAULT_CHANNEL_NAME, + NotificationManager.IMPORTANCE_LOW + ).apply { + enableLights(true) + setShowBadge(true) + } + } + manager.createNotificationChannel(channel) + } + } +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/view/NumberProgressBar.java b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/view/NumberProgressBar.java new file mode 100644 index 000000000..4f6a3fa4a --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/view/NumberProgressBar.java @@ -0,0 +1,479 @@ +package com.tongdaxing.erban.upgrade.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.View; + +import com.mango.moshen.R; + + +/** + * Created by daimajia on 14-4-30. + * + */ +public class NumberProgressBar extends View { + + private int mMaxProgress = 100; + + /** + * Current progress, can not exceed the max progress. + */ + private int mCurrentProgress = 0; + + /** + * The progress area bar color. + */ + private int mReachedBarColor; + + /** + * The bar unreached area color. + */ + private int mUnreachedBarColor; + + /** + * The progress text color. + */ + private int mTextColor; + + /** + * The progress text size. + */ + private float mTextSize; + + /** + * The height of the reached area. + */ + private float mReachedBarHeight; + + /** + * The height of the unreached area. + */ + private float mUnreachedBarHeight; + + /** + * The suffix of the number. + */ + private String mSuffix = "%"; + + /** + * The prefix. + */ + private String mPrefix = ""; + + + private final int default_text_color = Color.rgb(255, 137, 91); + private final int default_reached_color = Color.rgb(255, 137, 91); + private final int default_unreached_color = Color.rgb(204, 204, 204); + private final float default_progress_text_offset; + private final float default_text_size; + + /** + * For save and restore instance of progressbar. + */ + private static final String INSTANCE_STATE = "saved_instance"; + private static final String INSTANCE_TEXT_COLOR = "text_color"; + private static final String INSTANCE_TEXT_SIZE = "text_size"; + private static final String INSTANCE_REACHED_BAR_HEIGHT = "reached_bar_height"; + private static final String INSTANCE_REACHED_BAR_COLOR = "reached_bar_color"; + private static final String INSTANCE_UNREACHED_BAR_HEIGHT = "unreached_bar_height"; + private static final String INSTANCE_UNREACHED_BAR_COLOR = "unreached_bar_color"; + private static final String INSTANCE_MAX = "max"; + private static final String INSTANCE_PROGRESS = "progress"; + private static final String INSTANCE_SUFFIX = "suffix"; + private static final String INSTANCE_PREFIX = "prefix"; + private static final String INSTANCE_TEXT_VISIBILITY = "text_visibility"; + + private static final int PROGRESS_TEXT_VISIBLE = 0; + + + /** + * The width of the text that to be drawn. + */ + private float mDrawTextWidth; + + /** + * The drawn text start. + */ + private float mDrawTextStart; + + /** + * The drawn text end. + */ + private float mDrawTextEnd; + + /** + * The text that to be drawn in onDraw(). + */ + private String mCurrentDrawText; + + /** + * The Paint of the reached area. + */ + private Paint mReachedBarPaint; + /** + * The Paint of the unreached area. + */ + private Paint mUnreachedBarPaint; + /** + * The Paint of the progress text. + */ + private Paint mTextPaint; + + /** + * Unreached bar area to draw rect. + */ + private RectF mUnreachedRectF = new RectF(0, 0, 0, 0); + /** + * Reached bar area rect. + */ + private RectF mReachedRectF = new RectF(0, 0, 0, 0); + + /** + * The progress text offset. + */ + private float mOffset; + + /** + * Determine if need to draw unreached area. + */ + private boolean mDrawUnreachedBar = true; + + private boolean mDrawReachedBar = true; + + private boolean mIfDrawText = true; + + public enum ProgressTextVisibility { + Visible, Invisible + } + + public NumberProgressBar(Context context) { + this(context, null); + } + + public NumberProgressBar(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NumberProgressBar(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + mReachedBarHeight = dp2px(1.5f); + mUnreachedBarHeight = dp2px(1.0f); + default_text_size = sp2px(10); + default_progress_text_offset = dp2px(3.0f); + + //load styled attributes. + final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.NumberProgressBar, + defStyleAttr, 0); + + mReachedBarColor = attributes.getColor(R.styleable.NumberProgressBar_progress_reached_color, default_reached_color); + mUnreachedBarColor = attributes.getColor(R.styleable.NumberProgressBar_progress_unreached_color, default_unreached_color); + mTextColor = attributes.getColor(R.styleable.NumberProgressBar_progress_text_color, default_text_color); + mTextSize = attributes.getDimension(R.styleable.NumberProgressBar_progress_text_size, default_text_size); + attributes.recycle(); + initializePainters(); + } + + @Override + protected int getSuggestedMinimumWidth() { + return (int) mTextSize; + } + + @Override + protected int getSuggestedMinimumHeight() { + return Math.max((int) mTextSize, Math.max((int) mReachedBarHeight, (int) mUnreachedBarHeight)); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(measure(widthMeasureSpec, true), measure(heightMeasureSpec, false)); + } + + private int measure(int measureSpec, boolean isWidth) { + int result; + int mode = MeasureSpec.getMode(measureSpec); + int size = MeasureSpec.getSize(measureSpec); + int padding = isWidth ? getPaddingLeft() + getPaddingRight() : getPaddingTop() + getPaddingBottom(); + if (mode == MeasureSpec.EXACTLY) { + result = size; + } else { + result = isWidth ? getSuggestedMinimumWidth() : getSuggestedMinimumHeight(); + result += padding; + if (mode == MeasureSpec.AT_MOST) { + if (isWidth) { + result = Math.max(result, size); + } else { + result = Math.min(result, size); + } + } + } + return result; + } + + @Override + protected void onDraw(Canvas canvas) { + if (mIfDrawText) { + calculateDrawRectF(); + } else { + calculateDrawRectFWithoutProgressText(); + } + + if (mDrawReachedBar) { + canvas.drawRect(mReachedRectF, mReachedBarPaint); + } + + if (mDrawUnreachedBar) { + canvas.drawRect(mUnreachedRectF, mUnreachedBarPaint); + } + + if (mIfDrawText) + canvas.drawText(mCurrentDrawText, mDrawTextStart, mDrawTextEnd, mTextPaint); + } + + private void initializePainters() { + mReachedBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mReachedBarPaint.setColor(mReachedBarColor); + + mUnreachedBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mUnreachedBarPaint.setColor(mUnreachedBarColor); + + mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setColor(mTextColor); + mTextPaint.setTextSize(mTextSize); + } + + + private void calculateDrawRectFWithoutProgressText() { + mReachedRectF.left = getPaddingLeft(); + mReachedRectF.top = getHeight() / 2.0f - mReachedBarHeight / 2.0f; + mReachedRectF.right = (getWidth() - getPaddingLeft() - getPaddingRight()) / (getMax() * 1.0f) * getProgress() + getPaddingLeft(); + mReachedRectF.bottom = getHeight() / 2.0f + mReachedBarHeight / 2.0f; + + mUnreachedRectF.left = mReachedRectF.right; + mUnreachedRectF.right = getWidth() - getPaddingRight(); + mUnreachedRectF.top = getHeight() / 2.0f + -mUnreachedBarHeight / 2.0f; + mUnreachedRectF.bottom = getHeight() / 2.0f + mUnreachedBarHeight / 2.0f; + } + + private void calculateDrawRectF() { + + mCurrentDrawText = String.format("%d", getProgress() * 100 / getMax()); + mCurrentDrawText = mPrefix + mCurrentDrawText + mSuffix; + mDrawTextWidth = mTextPaint.measureText(mCurrentDrawText); + + if (getProgress() == 0) { + mDrawReachedBar = false; + mDrawTextStart = getPaddingLeft(); + } else { + mDrawReachedBar = true; + mReachedRectF.left = getPaddingLeft(); + mReachedRectF.top = getHeight() / 2.0f - mReachedBarHeight / 2.0f; + mReachedRectF.right = (getWidth() - getPaddingLeft() - getPaddingRight()) / (getMax() * 1.0f) * getProgress() - mOffset + getPaddingLeft(); + mReachedRectF.bottom = getHeight() / 2.0f + mReachedBarHeight / 2.0f; + mDrawTextStart = (mReachedRectF.right + mOffset); + } + + mDrawTextEnd = (int) ((getHeight() / 2.0f) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2.0f)); + + if ((mDrawTextStart + mDrawTextWidth) >= getWidth() - getPaddingRight()) { + mDrawTextStart = getWidth() - getPaddingRight() - mDrawTextWidth; + mReachedRectF.right = mDrawTextStart - mOffset; + } + + float unreachedBarStart = mDrawTextStart + mDrawTextWidth + mOffset; + if (unreachedBarStart >= getWidth() - getPaddingRight()) { + mDrawUnreachedBar = false; + } else { + mDrawUnreachedBar = true; + mUnreachedRectF.left = unreachedBarStart; + mUnreachedRectF.right = getWidth() - getPaddingRight(); + mUnreachedRectF.top = getHeight() / 2.0f + -mUnreachedBarHeight / 2.0f; + mUnreachedRectF.bottom = getHeight() / 2.0f + mUnreachedBarHeight / 2.0f; + } + } + + /** + * Get progress text color. + * + * @return progress text color. + */ + public int getTextColor() { + return mTextColor; + } + + /** + * Get progress text size. + * + * @return progress text size. + */ + public float getProgressTextSize() { + return mTextSize; + } + + public int getUnreachedBarColor() { + return mUnreachedBarColor; + } + + public int getReachedBarColor() { + return mReachedBarColor; + } + + public int getProgress() { + return mCurrentProgress; + } + + public int getMax() { + return mMaxProgress; + } + + public float getReachedBarHeight() { + return mReachedBarHeight; + } + + public float getUnreachedBarHeight() { + return mUnreachedBarHeight; + } + + public void setProgressTextSize(float textSize) { + this.mTextSize = textSize; + mTextPaint.setTextSize(mTextSize); + invalidate(); + } + + public void setProgressTextColor(int textColor) { + this.mTextColor = textColor; + mTextPaint.setColor(mTextColor); + invalidate(); + } + + public void setUnreachedBarColor(int barColor) { + this.mUnreachedBarColor = barColor; + mUnreachedBarPaint.setColor(mUnreachedBarColor); + invalidate(); + } + + public void setReachedBarColor(int progressColor) { + this.mReachedBarColor = progressColor; + mReachedBarPaint.setColor(mReachedBarColor); + invalidate(); + } + + public void setReachedBarHeight(float height) { + mReachedBarHeight = height; + } + + public void setUnreachedBarHeight(float height) { + mUnreachedBarHeight = height; + } + + public void setMax(int maxProgress) { + if (maxProgress > 0) { + this.mMaxProgress = maxProgress; + invalidate(); + } + } + + public void setSuffix(String suffix) { + if (suffix == null) { + mSuffix = ""; + } else { + mSuffix = suffix; + } + } + + public String getSuffix() { + return mSuffix; + } + + public void setPrefix(String prefix) { + if (prefix == null) + mPrefix = ""; + else { + mPrefix = prefix; + } + } + + public String getPrefix() { + return mPrefix; + } + + public void incrementProgressBy(int by) { + if (by > 0) { + setProgress(getProgress() + by); + } + } + + public void setProgress(int progress) { + if (progress <= getMax() && progress >= 0) { + this.mCurrentProgress = progress; + invalidate(); + } + } + + @Override + protected Parcelable onSaveInstanceState() { + final Bundle bundle = new Bundle(); + bundle.putParcelable(INSTANCE_STATE, super.onSaveInstanceState()); + bundle.putInt(INSTANCE_TEXT_COLOR, getTextColor()); + bundle.putFloat(INSTANCE_TEXT_SIZE, getProgressTextSize()); + bundle.putFloat(INSTANCE_REACHED_BAR_HEIGHT, getReachedBarHeight()); + bundle.putFloat(INSTANCE_UNREACHED_BAR_HEIGHT, getUnreachedBarHeight()); + bundle.putInt(INSTANCE_REACHED_BAR_COLOR, getReachedBarColor()); + bundle.putInt(INSTANCE_UNREACHED_BAR_COLOR, getUnreachedBarColor()); + bundle.putInt(INSTANCE_MAX, getMax()); + bundle.putInt(INSTANCE_PROGRESS, getProgress()); + bundle.putString(INSTANCE_SUFFIX, getSuffix()); + bundle.putString(INSTANCE_PREFIX, getPrefix()); + bundle.putBoolean(INSTANCE_TEXT_VISIBILITY, getProgressTextVisibility()); + return bundle; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (state instanceof Bundle) { + final Bundle bundle = (Bundle) state; + mTextColor = bundle.getInt(INSTANCE_TEXT_COLOR); + mTextSize = bundle.getFloat(INSTANCE_TEXT_SIZE); + mReachedBarHeight = bundle.getFloat(INSTANCE_REACHED_BAR_HEIGHT); + mUnreachedBarHeight = bundle.getFloat(INSTANCE_UNREACHED_BAR_HEIGHT); + mReachedBarColor = bundle.getInt(INSTANCE_REACHED_BAR_COLOR); + mUnreachedBarColor = bundle.getInt(INSTANCE_UNREACHED_BAR_COLOR); + initializePainters(); + setMax(bundle.getInt(INSTANCE_MAX)); + setProgress(bundle.getInt(INSTANCE_PROGRESS)); + setPrefix(bundle.getString(INSTANCE_PREFIX)); + setSuffix(bundle.getString(INSTANCE_SUFFIX)); + setProgressTextVisibility(bundle.getBoolean(INSTANCE_TEXT_VISIBILITY) ? ProgressTextVisibility.Visible : ProgressTextVisibility.Invisible); + super.onRestoreInstanceState(bundle.getParcelable(INSTANCE_STATE)); + return; + } + super.onRestoreInstanceState(state); + } + + public float dp2px(float dp) { + final float scale = getResources().getDisplayMetrics().density; + return dp * scale + 0.5f; + } + + public float sp2px(float sp) { + final float scale = getResources().getDisplayMetrics().scaledDensity; + return sp * scale; + } + + public void setProgressTextVisibility(ProgressTextVisibility visibility) { + mIfDrawText = visibility == ProgressTextVisibility.Visible; + invalidate(); + } + + public boolean getProgressTextVisibility() { + return mIfDrawText; + } + +} diff --git a/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/view/UpdateDialogActivity.kt b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/view/UpdateDialogActivity.kt new file mode 100644 index 000000000..92bb71c4c --- /dev/null +++ b/app/src/module_upgrade_app/java/com/tongdaxing/erban/upgrade/view/UpdateDialogActivity.kt @@ -0,0 +1,197 @@ +package com.tongdaxing.erban.upgrade.view + +import android.content.Intent +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.StateListDrawable +import android.os.Bundle +import android.view.Gravity +import android.view.View +import android.view.WindowManager +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import com.mango.moshen.R +import com.tongdaxing.erban.upgrade.config.Constant +import com.tongdaxing.erban.upgrade.listener.OnButtonClickListener +import com.tongdaxing.erban.upgrade.listener.OnDownloadListenerAdapter +import com.tongdaxing.erban.upgrade.manager.DownloadManager +import com.tongdaxing.erban.upgrade.service.DownloadService +import com.tongdaxing.erban.upgrade.util.ApkUtil +import com.tongdaxing.erban.upgrade.util.DensityUtil +import com.tongdaxing.erban.upgrade.util.LogUtil +import java.io.File + + +/** + * ProjectName: AppUpdate + * PackageName: com.tongdaxing.erban.upgrade.view + * FileName: UpdateDialogActivity + * CreateDate: 2022/4/7 on 17:40 + * Desc: + * + * @author azhon + */ + +class UpdateDialogActivity : AppCompatActivity(), View.OnClickListener { + + private val install = 0x45 + private val error = 0x46 + private lateinit var manager: DownloadManager + private lateinit var apk: File + private lateinit var progressBar: NumberProgressBar + private lateinit var btnUpdate: Button + + companion object { + private const val TAG = "UpdateDialogActivity" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + overridePendingTransition(0, 0) + title = "" + setContentView(R.layout.dialog_update) + init() + } + + private fun init() { + val tempManager = DownloadManager.getInstance() + if (tempManager == null) { + LogUtil.e(TAG, "An exception occurred by DownloadManager=null,please check your code!") + return + } + manager = tempManager + if (manager.forcedUpgrade) { + manager.onDownloadListeners.add(listenerAdapter) + } + setWindowSize() + initView() + } + + private fun initView() { + val ibClose = findViewById(R.id.ib_close) + val vLine = findViewById(R.id.line) + val ivBg = findViewById(R.id.iv_bg) + val tvTitle = findViewById(R.id.tv_title) + val tvSize = findViewById(R.id.tv_size) + val tvDescription = findViewById(R.id.tv_description) + progressBar = findViewById(R.id.np_bar) + btnUpdate = findViewById(R.id.btn_update) + progressBar.visibility = if (manager.forcedUpgrade) View.VISIBLE else View.GONE + btnUpdate.tag = 0 + btnUpdate.setOnClickListener(this) + ibClose.setOnClickListener(this) + if (manager.dialogImage != -1) { + ivBg.setBackgroundResource(manager.dialogImage) + } + if (manager.dialogButtonTextColor != -1) { + btnUpdate.setTextColor(manager.dialogButtonTextColor) + } + if (manager.dialogProgressBarColor != -1) { + progressBar.reachedBarColor = manager.dialogProgressBarColor + progressBar.setProgressTextColor(manager.dialogProgressBarColor) + } + if (manager.dialogButtonColor != -1) { + val colorDrawable = GradientDrawable().apply { + setColor(manager.dialogButtonColor) + cornerRadius = DensityUtil.dip2px(this@UpdateDialogActivity, 3f) + } + val drawable = StateListDrawable().apply { + addState(intArrayOf(android.R.attr.state_pressed), colorDrawable) + addState(IntArray(0), colorDrawable) + } + btnUpdate.background = drawable + } + if (manager.forcedUpgrade) { + vLine.visibility = View.GONE + ibClose.visibility = View.GONE + } + if (manager.apkVersionName.isNotEmpty()) { + tvTitle.text = + String.format(resources.getString(R.string.dialog_new), manager.apkVersionName) + } + if (manager.apkSize.isNotEmpty()) { + tvSize.text = + String.format(resources.getString(R.string.dialog_new_size), manager.apkSize) + tvSize.visibility = View.VISIBLE + } + tvDescription.text = manager.apkDescription + } + + private fun setWindowSize() { + val attributes = window.attributes + attributes.width = (resources.displayMetrics.widthPixels * 0.75f).toInt() + attributes.height = WindowManager.LayoutParams.WRAP_CONTENT + attributes.gravity = Gravity.CENTER + window.attributes = attributes + } + + override fun onClick(v: View?) { + when (v?.id) { + R.id.ib_close -> { + if (!manager.forcedUpgrade) { + finish() + } + manager.onButtonClickListener?.onButtonClick(OnButtonClickListener.CANCEL) + } + R.id.btn_update -> { + if (btnUpdate.tag == install) { + ApkUtil.installApk(this, Constant.AUTHORITIES!!, apk) + return + } + if (manager.forcedUpgrade) { + btnUpdate.isEnabled = false + btnUpdate.text = resources.getString(R.string.background_downloading) + } else { + finish() + } + manager.onButtonClickListener?.onButtonClick(OnButtonClickListener.UPDATE) + startService(Intent(this, DownloadService::class.java)) + } + } + } + + override fun onBackPressed() { + if (manager.forcedUpgrade) return + super.onBackPressed() + } + + override fun finish() { + super.finish() + overridePendingTransition(0, 0) + } + + private val listenerAdapter: OnDownloadListenerAdapter = object : OnDownloadListenerAdapter() { + override fun start() { + btnUpdate.isEnabled = false + btnUpdate.text = resources.getString(R.string.background_downloading) + } + + override fun downloading(max: Int, progress: Int) { + if (max != -1) { + val curr = (progress / max.toDouble() * 100.0).toInt() + progressBar.progress = curr + } else { + progressBar.visibility = View.GONE + } + } + + override fun done(apk: File) { + this@UpdateDialogActivity.apk = apk + btnUpdate.tag = install + btnUpdate.isEnabled = true + btnUpdate.text = resources.getString(R.string.click_hint) + } + + override fun error(e: Throwable) { + btnUpdate.tag = error + btnUpdate.isEnabled = true + btnUpdate.text = resources.getString(R.string.continue_downloading) + } + } + + override fun onDestroy() { + super.onDestroy() + manager.onDownloadListeners.remove(listenerAdapter) + } +} \ No newline at end of file diff --git a/app/src/module_upgrade_app/res/drawable/bg_button.xml b/app/src/module_upgrade_app/res/drawable/bg_button.xml new file mode 100644 index 000000000..ffb9ab3c4 --- /dev/null +++ b/app/src/module_upgrade_app/res/drawable/bg_button.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/module_upgrade_app/res/drawable/bg_white_radius_6.xml b/app/src/module_upgrade_app/res/drawable/bg_white_radius_6.xml new file mode 100644 index 000000000..1d9623652 --- /dev/null +++ b/app/src/module_upgrade_app/res/drawable/bg_white_radius_6.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/module_upgrade_app/res/drawable/ic_dialog_close.png b/app/src/module_upgrade_app/res/drawable/ic_dialog_close.png new file mode 100644 index 0000000000000000000000000000000000000000..03315a3adebc61e4e915c00ae4b523d246ec6e36 GIT binary patch literal 4434 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 literal 0 HcmV?d00001 diff --git a/app/src/module_upgrade_app/res/drawable/ic_dialog_default.png b/app/src/module_upgrade_app/res/drawable/ic_dialog_default.png new file mode 100644 index 0000000000000000000000000000000000000000..2b036c811408ff08c46fe6c526025d7e04790ae0 GIT binary patch literal 38961 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
@s0Lmb2(~Q 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 Ko^|e_=l%o9Yc!w$ literal 0 HcmV?d00001 diff --git a/app/src/module_upgrade_app/res/layout/dialog_update.xml b/app/src/module_upgrade_app/res/layout/dialog_update.xml new file mode 100644 index 000000000..013d30c29 --- /dev/null +++ b/app/src/module_upgrade_app/res/layout/dialog_update.xml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + +