强更替换为appUpdate库。
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
40
appupdate/build.gradle
Normal 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'
|
||||
}
|
||||
0
appupdate/consumer-rules.pro
Normal file
0
appupdate/consumer-rules.pro
Normal file
1
appupdate/gradle.properties
Normal file
1
appupdate/gradle.properties
Normal file
@@ -0,0 +1 @@
|
||||
ARTIFACT_ID=appupdate
|
||||
21
appupdate/proguard-rules.pro
vendored
Normal file
21
appupdate/proguard-rules.pro
vendored
Normal 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
|
||||
25
appupdate/src/main/AndroidManifest.xml
Normal file
25
appupdate/src/main/AndroidManifest.xml
Normal 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>
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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)!!
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
80
appupdate/src/main/java/com/azhon/appupdate/util/ApkUtil.kt
Normal file
80
appupdate/src/main/java/com/azhon/appupdate/util/ApkUtil.kt
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
43
appupdate/src/main/java/com/azhon/appupdate/util/FileUtil.kt
Normal file
43
appupdate/src/main/java/com/azhon/appupdate/util/FileUtil.kt
Normal 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 ""
|
||||
}
|
||||
}
|
||||
}
|
||||
36
appupdate/src/main/java/com/azhon/appupdate/util/LogUtil.kt
Normal file
36
appupdate/src/main/java/com/azhon/appupdate/util/LogUtil.kt
Normal 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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
15
appupdate/src/main/res/drawable/app_update_bg_button.xml
Normal file
15
appupdate/src/main/res/drawable/app_update_bg_button.xml
Normal 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>
|
||||
@@ -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>
|
||||
BIN
appupdate/src/main/res/drawable/app_update_dialog_close.png
Normal file
BIN
appupdate/src/main/res/drawable/app_update_dialog_close.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.3 KiB |
BIN
appupdate/src/main/res/drawable/app_update_dialog_default.png
Normal file
BIN
appupdate/src/main/res/drawable/app_update_dialog_default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
97
appupdate/src/main/res/layout/app_update_dialog_update.xml
Normal file
97
appupdate/src/main/res/layout/app_update_dialog_update.xml
Normal 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>
|
||||
15
appupdate/src/main/res/values-zh-rTW/strings.xml
Normal file
15
appupdate/src/main/res/values-zh-rTW/strings.xml
Normal 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>
|
||||
15
appupdate/src/main/res/values/strings.xml
Normal file
15
appupdate/src/main/res/values/strings.xml
Normal 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>
|
||||
25
appupdate/src/main/res/values/styles.xml
Normal file
25
appupdate/src/main/res/values/styles.xml
Normal 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>
|
||||
9
appupdate/src/main/res/xml/app_update_file.xml
Normal file
9
appupdate/src/main/res/xml/app_update_file.xml
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -64,3 +64,4 @@ include ':tuicore'
|
||||
include ':Loadinglibrary'
|
||||
include 'locktableview'
|
||||
include ':animplayer'
|
||||
include ':appupdate'
|
||||
|
||||
Reference in New Issue
Block a user