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 000000000..03315a3ad Binary files /dev/null and b/app/src/module_upgrade_app/res/drawable/ic_dialog_close.png differ 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 000000000..2b036c811 Binary files /dev/null and b/app/src/module_upgrade_app/res/drawable/ic_dialog_default.png differ 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 @@ + + + + + + + + + + + + + + + + + + +