强更替换为appUpdate库。

This commit is contained in:
2025-12-23 18:20:25 +08:00
parent bb54407c62
commit 214c339e93
39 changed files with 2503 additions and 18 deletions

View File

@@ -9,7 +9,6 @@ import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.util.Log;
import com.alibaba.android.arouter.utils.TextUtils;
import com.blankj.utilcode.util.FileUtils;
@@ -230,14 +229,14 @@ public class DownloadUtil {
}
}
if (TextUtils.isEmpty(mApkPath)) {
Log.e(TAG, "downloadApk: 存储路径为空了");
LogUtils.e(TAG, "downloadApk: 存储路径为空了");
return;
}
//建立一个文件
mFile = new File(mApkPath);
if (FileUtils.createFileByDeleteOldFile(mFile)) {
if (mApi == null) {
Log.e(TAG, "downloadApk: 下载接口为空了");
LogUtils.e(TAG, "downloadApk: 下载接口为空了");
return;
}
mCall = mApi.downloadFile(url);
@@ -284,7 +283,7 @@ public class DownloadUtil {
while ((len = is.read(buff)) != -1) {
os.write(buff, 0, len);
currentLength += len;
Log.e(TAG, "当前进度: " + currentLength);
LogUtils.e(TAG, "当前进度: " + currentLength);
long finalCurrentLength = currentLength;
HANDLER.post(new Runnable() {
@Override

View File

@@ -58,7 +58,6 @@ dependencies {
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
implementation project(':BaseModule')
implementation (libs.arouter.api.v150)
//annotationProcessor
@@ -70,6 +69,7 @@ dependencies {
api project(':tuiconversation')
api project(':tuichat')
api project(':BaseModule')
api project(':appupdate')
}

View File

@@ -43,11 +43,17 @@ open class Application : CommonAppContext() {
var inviteDialog: InviteDialog? = null
var currDialogActivity: Activity? = null
// 单例实例
companion object {
@Volatile
private lateinit var instance: Application
var isKeepScreenOn = false
// 全局获取 Application 实例
fun getInstance(): Application {
return instance
@@ -232,6 +238,8 @@ open class Application : CommonAppContext() {
}
fun showInviteDialog(activity: Activity?, t: IndexRecommendRoom) {
if (isKeepScreenOn)
return
if (activity != null && activity == currDialogActivity && inviteDialog != null) {
inviteDialog?.setData(t)
return

View File

@@ -19,6 +19,7 @@ import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
@@ -31,6 +32,9 @@ import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import com.alibaba.android.arouter.facade.annotation.Route;
import com.azhon.appupdate.listener.OnButtonClickListener;
import com.azhon.appupdate.listener.OnDownloadListener;
import com.azhon.appupdate.manager.DownloadManager;
import com.blankj.utilcode.util.FragmentUtils;
import com.blankj.utilcode.util.LogUtils;
import com.blankj.utilcode.util.ToastUtils;
@@ -86,6 +90,7 @@ import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -504,6 +509,11 @@ public class MainActivity extends BaseMvpActivity<HomePresenter, ActivityMainBin
MvpPre.activitiesPermission();//获取悬浮框权限
if (manager != null && !manager.getDownloadState()){
manager.download();
}
}
private void requestGpsPermissions() {
@@ -662,22 +672,97 @@ public class MainActivity extends BaseMvpActivity<HomePresenter, ActivityMainBin
MvpPre.userNews();
}
// 用于记录是否手动设置了禁止息屏,避免影响其他场景
/**
* 控制屏幕是否保持常亮(禁止/恢复自动息屏)
*
* @param keepOn true=禁止息屏false=恢复自动息屏
*/
private void keepScreenOn(boolean keepOn) {
if (keepOn == Application.Companion.isKeepScreenOn()) {
return; // 避免重复设置
}
Application.Companion.setKeepScreenOn(keepOn);
// 获取当前Activity的Window设置FLAG_KEEP_SCREEN_ON
if (keepOn) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
@Override
public void appUpdate(AppUpdateModel appUpdateModel) {
if (appUpdateModel.getCode() > getCurrentVersionCode(this)) {
if (appUpdateDialog == null) {
appUpdateDialog = new AppUpdateDialog(this);
appUpdateDialog.setAppUpdateModel(appUpdateModel);
if (appUpdateModel.getIs_force().equals("1") || getCurrentVersionCode(this) - appUpdateModel.getCode() >= 2) {
appUpdateDialog.setCanceledOnTouchOutside(false);
// 1. 禁止屏幕自动息屏(核心逻辑)
keepScreenOn(true);
// 初始化DownloadManager注意需确保DownloadManager类的包名正确
manager = new DownloadManager.Builder(this)
.apkUrl(appUpdateModel.getUrl())
.apkName("yusheng.apk")
.smallIcon(R.mipmap.ic_launcher_foreground)
.showNewerToast(false)
.apkVersionCode(appUpdateModel.getCode())
.apkVersionName(appUpdateModel.getVersion())
.apkDescription(appUpdateModel.getContent())
.enableLog(true)
.jumpInstallPage(true)
.dialogButtonTextColor(Color.WHITE)
.showNotification(true)
.showBgdToast(false)
.dialogImage(com.xscm.moduleutil.R.mipmap.imh_app_update)
.forcedUpgrade(appUpdateModel.getIs_force().equals("1"))
.onDownloadListener(new OnDownloadListener() {
@Override
public void start() {
}
@Override
public void downloading(int max, int progress) {
LogUtils.e("AppUpdate", "downloading"+progress);
}
appUpdateDialog.show();
@Override
public void done(@NonNull File apk) {
LogUtils.e("AppUpdate", "done");
}
@Override
public void cancel() {
LogUtils.e("AppUpdate", "cancel");
}
@Override
public void error(@NonNull Throwable e) {
LogUtils.e("AppUpdate", "error", e);
}
})
.onButtonClickListener(id -> {
LogUtils.e("TAG", "onButtonClick: " + id);
})
.build();
// 判空后执行下载
if (manager != null) {
manager.download();
}
// if (appUpdateDialog == null) {
// appUpdateDialog = new AppUpdateDialog(this);
// appUpdateDialog.setAppUpdateModel(appUpdateModel);
// if (appUpdateModel.getIs_force().equals("1") || getCurrentVersionCode(this) - appUpdateModel.getCode() >= 2) {
// appUpdateDialog.setCanceledOnTouchOutside(false);
// }
// }
// appUpdateDialog.show();
}
}
private DownloadManager manager = null;
private String city1;
@Override
@@ -696,7 +781,7 @@ public class MainActivity extends BaseMvpActivity<HomePresenter, ActivityMainBin
}
public static int getCurrentVersionCode(Context context) {
public int getCurrentVersionCode(Context context) {
try {
PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return packageInfo.versionCode;

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

40
appupdate/build.gradle Normal file
View File

@@ -0,0 +1,40 @@
apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android'
android {
compileSdk 33
namespace 'com.azhon.appupdate'
defaultConfig {
minSdk 16
targetSdk 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1"
implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.2'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

View File

View File

@@ -0,0 +1 @@
ARTIFACT_ID=appupdate

21
appupdate/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.azhon.appupdate">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application android:usesCleartextTraffic="true">
<service android:name=".service.DownloadService" />
<provider
android:name="com.azhon.appupdate.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=".view.UpdateDialogActivity"
android:theme="@style/AppUpdate.UpdateDialog" />
</application>
</manifest>

View File

@@ -0,0 +1,31 @@
package com.azhon.appupdate.base
import com.azhon.appupdate.base.bean.DownloadStatus
import kotlinx.coroutines.flow.Flow
/**
* 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,24 @@
package com.azhon.appupdate.base.bean
import java.io.File
/**
* 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,13 @@
package com.azhon.appupdate.config
import androidx.core.content.FileProvider
/**
* createDate: 2022/4/7 on 10:30
* desc:
*
* @author azhon
*/
class AppUpdateFileProvider : FileProvider()

View File

@@ -0,0 +1,56 @@
package com.azhon.appupdate.config
/**
* 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,37 @@
package com.azhon.appupdate.listener
import android.app.Activity
import android.app.Application
import android.os.Bundle
/**
* 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,25 @@
package com.azhon.appupdate.listener
/**
* 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,40 @@
package com.azhon.appupdate.listener
import java.io.File
/**
* 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,28 @@
package com.azhon.appupdate.listener
import java.io.File
/**
* 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,439 @@
package com.azhon.appupdate.manager
import android.app.Activity
import android.app.Application
import android.app.NotificationChannel
import android.content.Intent
import android.widget.Toast
import com.azhon.appupdate.R
import com.azhon.appupdate.base.BaseHttpDownloadManager
import com.azhon.appupdate.config.Constant
import com.azhon.appupdate.listener.LifecycleCallbacksAdapter
import com.azhon.appupdate.listener.OnButtonClickListener
import com.azhon.appupdate.listener.OnDownloadListener
import com.azhon.appupdate.service.DownloadService
import com.azhon.appupdate.util.ApkUtil
import com.azhon.appupdate.util.LogUtil
import com.azhon.appupdate.view.UpdateDialogActivity
import java.io.Serializable
/**
* 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
internal fun getInstance(builder: Builder? = null): DownloadManager? {
if (instance == null) {
if (builder == null) return null
instance = DownloadManager(builder)
}
return instance!!
}
}
private var application: Application = builder.application
private var apkVersionCode: Int
private var showNewerToast: Boolean
internal var contextClsName: String = builder.contextClsName
internal var apkUrl: String
internal var apkName: String
internal var apkVersionName: String
internal var downloadPath: String
internal var smallIcon: Int
internal var apkDescription: String
internal var apkSize: String
internal var apkMD5: String
internal var httpManager: BaseHttpDownloadManager?
internal var notificationChannel: NotificationChannel?
internal var onDownloadListeners: MutableList<OnDownloadListener>
internal var onButtonClickListener: OnButtonClickListener?
internal var showNotification: Boolean
internal var jumpInstallPage: Boolean
internal var showBgdToast: Boolean
internal var forcedUpgrade: Boolean
internal var notifyId: Int
internal var dialogImage: Int
internal var dialogButtonColor: Int
internal var dialogButtonTextColor: Int
internal var dialogProgressBarColor: Int
var downloadState: Boolean = false
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.app_update_latest_version, Toast.LENGTH_SHORT
).show()
}
LogUtil.d(TAG, application.resources.getString(R.string.app_update_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
* Call this method when you need to destroy the previous download and re-download,
* otherwise don't use it.
*/
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,132 @@
package com.azhon.appupdate.manager
import com.azhon.appupdate.base.BaseHttpDownloadManager
import com.azhon.appupdate.base.bean.DownloadStatus
import com.azhon.appupdate.config.Constant
import com.azhon.appupdate.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
/**
* 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.azhon.appupdate.service
import android.app.Service
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.widget.Toast
import com.azhon.appupdate.R
import com.azhon.appupdate.base.bean.DownloadStatus
import com.azhon.appupdate.config.Constant
import com.azhon.appupdate.listener.OnDownloadListener
import com.azhon.appupdate.manager.DownloadManager
import com.azhon.appupdate.manager.HttpDownloadManager
import com.azhon.appupdate.util.ApkUtil
import com.azhon.appupdate.util.FileUtil
import com.azhon.appupdate.util.LogUtil
import com.azhon.appupdate.util.NotificationUtil
import kotlinx.coroutines.*
import java.io.File
/**
* 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 {
if (manager.apkMD5.isBlank()) {
return false
}
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 don't 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.app_update_background_downloading, Toast.LENGTH_SHORT)
.show()
}
if (manager.showNotification) {
NotificationUtil.showNotification(
this@DownloadService, manager.smallIcon,
resources.getString(R.string.app_update_start_download),
resources.getString(R.string.app_update_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.app_update_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.app_update_download_completed),
resources.getString(R.string.app_update_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.app_update_download_error),
resources.getString(R.string.app_update_continue_downloading),
)
}
manager.onDownloadListeners.forEach { it.error(e) }
}
private fun releaseResources() {
manager.release()
stopSelf()
}
}

View File

@@ -0,0 +1,80 @@
package com.azhon.appupdate.util
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.PatternMatcher
import androidx.core.content.FileProvider
import java.io.File
/**
* 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,20 @@
package com.azhon.appupdate.util
import android.content.Context
/**
* 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,43 @@
package com.azhon.appupdate.util
import java.io.File
import java.io.FileInputStream
import java.math.BigInteger
import java.security.MessageDigest
/**
* 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).padStart(32, '0').uppercase()
} catch (e: Exception) {
e.printStackTrace()
}
return ""
}
}
}

View File

@@ -0,0 +1,36 @@
package com.azhon.appupdate.util
import android.util.Log
import com.azhon.appupdate.config.Constant
/**
* 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,171 @@
package com.azhon.appupdate.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.azhon.appupdate.config.Constant
import com.azhon.appupdate.manager.DownloadManager
import com.azhon.appupdate.service.DownloadService
import java.io.File
/**
* 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
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
afterO(manager)
}
manager.cancel(DownloadManager.getInstance()?.notifyId ?: Constant.DEFAULT_NOTIFY_ID)
val intent = ApkUtil.createInstallIntent(context, authorities, apk)
val pi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}
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 = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}
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.azhon.appupdate.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.azhon.appupdate.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.AppUpdate_NumberProgressBar,
defStyleAttr, 0);
mReachedBarColor = attributes.getColor(R.styleable.AppUpdate_NumberProgressBar_progress_reached_color, default_reached_color);
mUnreachedBarColor = attributes.getColor(R.styleable.AppUpdate_NumberProgressBar_progress_unreached_color, default_unreached_color);
mTextColor = attributes.getColor(R.styleable.AppUpdate_NumberProgressBar_progress_text_color, default_text_color);
mTextSize = attributes.getDimension(R.styleable.AppUpdate_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,280 @@
package com.azhon.appupdate.view
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.StateListDrawable
import android.os.Build
import android.os.Bundle
import android.text.Html
import android.text.method.LinkMovementMethod
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.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import com.azhon.appupdate.R
import com.azhon.appupdate.config.Constant
import com.azhon.appupdate.listener.OnButtonClickListener
import com.azhon.appupdate.listener.OnDownloadListenerAdapter
import com.azhon.appupdate.manager.DownloadManager
import com.azhon.appupdate.service.DownloadService
import com.azhon.appupdate.util.ApkUtil
import com.azhon.appupdate.util.DensityUtil
import com.azhon.appupdate.util.LogUtil
import java.io.File
/**
* createDate: 2022/4/7 on 17:40
* desc:
*
* @author azhon
*/
class UpdateDialogActivity : AppCompatActivity(), View.OnClickListener {
private val install = 0x45
private val error = 0x46
private val permissionCode = 0x47
private var manager: DownloadManager? = null
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.app_update_dialog_update)
//system back button
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
backPressed()
}
})
init()
}
private fun init() {
manager = DownloadManager.getInstance()
if (manager == null) {
LogUtil.e(TAG, "An exception occurred by DownloadManager=null,please check your code!")
return
}
if (manager!!.forcedUpgrade) {
manager!!.onDownloadListeners.add(listenerAdapter)
}
setWindowSize()
initView(manager!!)
}
private fun initView(manager: DownloadManager) {
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.app_update_dialog_new), manager.apkVersionName
)
}
if (manager.apkSize.isNotEmpty()) {
tvSize.text = String.format(
resources.getString(R.string.app_update_dialog_new_size), manager.apkSize
)
tvSize.visibility = View.VISIBLE
}
// tvDescription.text = manager.apkDescription
setHtmlText(tvDescription, manager.apkDescription, false)
}
/**
* 显示HTML格式文本可控制链接点击
* @param textView 目标TextView
* @param htmlContent HTML内容字符串
* @param enableLinks 是否启用链接点击
*/
fun setHtmlText(textView: TextView?, htmlContent: String?, enableLinks: Boolean) {
if (textView == null || htmlContent == null) return
// 处理不同Android版本的HTML解析
val spannedText: CharSequence = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(htmlContent, Html.FROM_HTML_MODE_COMPACT)
} else {
// 兼容Android N以下版本
Html.fromHtml(htmlContent)
}
textView.text = spannedText
// 启用链接点击功能
if (enableLinks) {
textView.movementMethod = LinkMovementMethod.getInstance()
textView.highlightColor = Color.TRANSPARENT // 去除点击高亮
}
}
private fun setWindowSize() {
val attributes = window.attributes
attributes.width = DensityUtil.dip2px(this@UpdateDialogActivity, 280f).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 == false) {
finish()
}
manager?.onButtonClickListener?.onButtonClick(OnButtonClickListener.CANCEL)
}
R.id.btn_update -> {
if (btnUpdate.tag == install) {
ApkUtil.installApk(this, Constant.AUTHORITIES!!, apk)
return
}
if (!checkPermission()) {
startUpdate()
}
}
}
}
/**
* check Notification runtime permission [DownloadManager.showNotification] is true && when api>=33.
* @return false: can continue to download, true: request permission.
*/
private fun checkPermission(): Boolean {
if (manager?.showNotification == false) {
LogUtil.d(TAG, "checkPermission: manager.showNotification = false")
return false
}
if (ActivityCompat.checkSelfPermission(
this, Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
) {
LogUtil.d(TAG, "checkPermission: has permission")
return false
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
LogUtil.d(TAG, "checkPermission: request permission")
ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), permissionCode
)
return true
}
return false
}
private fun startUpdate() {
if (manager?.forcedUpgrade == true) {
btnUpdate.isEnabled = false
btnUpdate.text = resources.getString(R.string.app_update_background_downloading)
} else {
finish()
}
manager?.onButtonClickListener?.onButtonClick(OnButtonClickListener.UPDATE)
startService(Intent(this, DownloadService::class.java))
}
private fun backPressed() {
if (manager?.forcedUpgrade == true) return
finish()
manager?.onButtonClickListener?.onButtonClick(OnButtonClickListener.CANCEL)
}
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.app_update_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.app_update_click_hint)
}
override fun error(e: Throwable) {
btnUpdate.tag = error
btnUpdate.isEnabled = true
btnUpdate.text = resources.getString(R.string.app_update_continue_downloading)
}
}
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (permissionCode == requestCode) {
startUpdate()
}
}
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/app_update_dialog_default" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/app_update_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="180dp"
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.azhon.appupdate.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/app_update_bg_button"
android:text="@string/app_update_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/app_update_dialog_close"
android:contentDescription="@string/app_update_close" />
</LinearLayout>

View File

@@ -0,0 +1,15 @@
<resources>
<string name="app_update_latest_version">當前已是最新版本!</string>
<string name="app_update_start_download">開始下載</string>
<string name="app_update_start_download_hint">可稍後查看下載進度</string>
<string name="app_update_start_downloading">正在下載新版本</string>
<string name="app_update_download_completed">下載完成</string>
<string name="app_update_click_hint">點擊進行安裝</string>
<string name="app_update_download_error">下載出錯</string>
<string name="app_update_continue_downloading">點擊繼續下載</string>
<string name="app_update_background_downloading">正在後臺下載新版本…</string>
<string name="app_update_dialog_new">發現新版本%s可以下載啦</string>
<string name="app_update_dialog_new_size">新版本大小:%s</string>
<string name="app_update_update">升級</string>
<string name="app_update_close">關閉</string>
</resources>

View File

@@ -0,0 +1,15 @@
<resources>
<string name="app_update_latest_version">当前已是最新版本!</string>
<string name="app_update_start_download">开始下载</string>
<string name="app_update_start_download_hint">可稍后查看下载进度</string>
<string name="app_update_start_downloading">正在下载新版本</string>
<string name="app_update_download_completed">下载完成</string>
<string name="app_update_click_hint">点击进行安装</string>
<string name="app_update_download_error">下载出错</string>
<string name="app_update_continue_downloading">点击继续下载</string>
<string name="app_update_background_downloading">正在后台下载新版本…</string>
<string name="app_update_dialog_new">发现新版本%s可以下载啦</string>
<string name="app_update_dialog_new_size">新版本大小:%s</string>
<string name="app_update_update">升级</string>
<string name="app_update_close">关闭</string>
</resources>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppUpdate.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="AppUpdate.UpdateDialog" parent="AppUpdate.DialogActivity">
<item name="android:backgroundDimEnabled">true</item>
</style>
<declare-styleable name="AppUpdate.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

@@ -28,8 +28,8 @@ isBuildModule=false
#org.gradle.deamon=false
android.injected.testOnly=false
APP_VERSION_NAME=1.0.9.1
APP_VERSION_CODE=81
APP_VERSION_NAME=1.0.9.0
APP_VERSION_CODE=80
org.gradle.jvm.toolchain.useLegacyAdapters=false
#org.gradle.java.home=C\:\\Users\\qx\\.jdks\\ms-17.0.15

View File

@@ -64,3 +64,4 @@ include ':tuicore'
include ':Loadinglibrary'
include 'locktableview'
include ':animplayer'
include ':appupdate'