新增AppUpgrade功能

This commit is contained in:
huangjian
2022-11-01 16:49:21 +08:00
parent 3f702773c9
commit 301e017df9
30 changed files with 2283 additions and 17 deletions

View File

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

View File

@@ -270,8 +270,8 @@
<!-- 云信继承vivo推送 start-->
<service
android:name="com.vivo.push.sdk.service.CommandClientService"
android:permission="com.push.permission.UPSTAGESERVICE"
android:exported="true" />
android:exported="true"
android:permission="com.push.permission.UPSTAGESERVICE" />
<activity
android:name="com.vivo.push.sdk.LinkProxyClientActivity"
android:exported="false"
@@ -1286,6 +1286,25 @@
android:name=".vip.VipRankActivity"
android:screenOrientation="portrait" />
<service android:name="com.tongdaxing.erban.upgrade.service.DownloadService" />
<provider
android:name="com.tongdaxing.erban.upgrade.config.AppUpdateFileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/app_update_file" />
</provider>
<activity
android:name="com.tongdaxing.erban.upgrade.view.UpdateDialogActivity"
android:theme="@style/UpdateDialog" />
</application>
</manifest>

View File

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

View File

@@ -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<DownloadStatus>
/**
* cancel download apk
*/
abstract fun cancel()
/**
* release memory
*/
abstract fun release()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<OnDownloadListener>
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<OnDownloadListener>()
/**
* 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)!!
}
}
}

View File

@@ -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<DownloadStatus> {
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<DownloadStatus>
) {
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<X509Certificate> {
return arrayOf()
}
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
LogUtil.d(TAG, "checkClientTrusted")
}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, 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()
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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.
* <a href="https://github.com/daimajia/NumberProgressBar/}"/>
*/
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;
}
}

View File

@@ -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<View>(R.id.ib_close)
val vLine = findViewById<View>(R.id.line)
val ivBg = findViewById<ImageView>(R.id.iv_bg)
val tvTitle = findViewById<TextView>(R.id.tv_title)
val tvSize = findViewById<TextView>(R.id.tv_size)
val tvDescription = findViewById<TextView>(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)
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<corners android:radius="3dp" />
<solid android:color="#ff895b" />
</shape>
</item>
<item android:state_pressed="false">
<shape>
<corners android:radius="3dp" />
<solid android:color="#ff895b" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners
android:bottomLeftRadius="6dp"
android:bottomRightRadius="6dp" />
<solid android:color="@android:color/white" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:orientation="vertical">
<ImageView
android:id="@+id/iv_bg"
android:layout_width="match_parent"
android:layout_height="140dp"
android:background="@drawable/ic_dialog_default" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_white_radius_6"
android:orientation="vertical">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:textColor="@android:color/black"
android:textSize="15sp"
tools:text="发现新版v2.0.1可以下载啦!" />
<TextView
android:id="@+id/tv_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginRight="16dp"
android:textColor="#757575"
android:textSize="14sp"
android:visibility="gone"
tools:text="新版本大小5M" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginRight="16dp"
android:overScrollMode="never">
<TextView
android:id="@+id/tv_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingMultiplier="1.1"
android:textColor="#757575"
android:textSize="14sp"
tools:text="" />
</ScrollView>
<com.tongdaxing.erban.upgrade.view.NumberProgressBar
android:id="@+id/np_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginRight="16dp" />
<Button
android:id="@+id/btn_update"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginRight="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/bg_button"
android:text="@string/update"
android:textAllCaps="false"
android:textColor="@android:color/white" />
</LinearLayout>
<View
android:id="@+id/line"
android:layout_width="2dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:background="@android:color/white" />
<ImageButton
android:id="@+id/ib_close"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center"
android:background="@drawable/ic_dialog_close" />
</LinearLayout>

View File

@@ -3,4 +3,16 @@
<string name="tips_downloading_percent">·正在更新%s·</string>
<string name="title_upgrade_app">版本更新</string>
<string name="title_upgrade_app_en">VERSION UPDATE</string>
<string name="latest_version">当前已是最新版本!</string>
<string name="start_download">开始下载</string>
<string name="start_download_hint">可稍后查看下载进度</string>
<string name="start_downloading">正在下载新版本</string>
<string name="download_completed">下载完成</string>
<string name="click_hint">点击进行安装</string>
<string name="download_error">下载出错</string>
<string name="continue_downloading">点击继续下载</string>
<string name="background_downloading">正在后台下载新版本…</string>
<string name="dialog_new">发现新版本%s可以下载啦</string>
<string name="dialog_new_size">新版本大小:%s</string>
<string name="update">升级</string>
</resources>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="DialogActivity" parent="Theme.AppCompat">
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowFrame">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:backgroundDimEnabled">false</item>
</style>
<style name="UpdateDialog" parent="DialogActivity">
<item name="android:backgroundDimEnabled">true</item>
</style>
<declare-styleable name="NumberProgressBar">
<attr name="progress_unreached_color" format="color" />
<attr name="progress_reached_color" format="color" />
<attr name="progress_text_size" format="dimension" />
<attr name="progress_text_color" format="color" />
</declare-styleable>
</resources>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path
name="app_update_external"
path="/" />
<external-cache-path
name="app_update_cache"
path="/" />
</paths>

View File

@@ -25,5 +25,5 @@ only_arm64=true
channel_file=channel.txt
version_name=5.5.0
version_code=550
version_name=5.3.0
version_code=500