├── .github
└── workflows
│ └── android.yml
├── .gitignore
├── .idea
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── example
│ │ └── teprinciple
│ │ └── updateappdemo
│ │ └── ExampleInstrumentedTest.java
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── example
│ │ └── teprinciple
│ │ └── updateappdemo
│ │ ├── CheckMd5DemoActivity.kt
│ │ ├── JavaDemoActivity.java
│ │ ├── MainActivity.kt
│ │ └── SpanUtils.java
│ └── res
│ ├── drawable
│ ├── bg_btn.xml
│ ├── bg_custom_update.png
│ ├── bg_custom_update_dialog.9.png
│ ├── ic_close.png
│ ├── ic_logo.png
│ └── ic_update.png
│ ├── layout
│ ├── activity_java_demo.xml
│ ├── activity_main.xml
│ ├── check_md5_demo_activity.xml
│ └── view_update_dialog_custom.xml
│ ├── mipmap-xhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ └── ic_launcher.png
│ ├── values-w820dp
│ └── dimens.xml
│ └── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── img
├── demo.png
├── update_ui_change.png
├── update_ui_custom.png
├── update_ui_downloading.png
├── update_ui_fail.png
├── update_ui_force.png
├── update_ui_plentiful.png
└── update_ui_simple.png
├── readme
├── README_1.5.2.md
├── version.md
└── 自定义UI.md
├── settings.gradle
├── update.gif
├── update.jks
└── updateapputils
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
├── androidTest
└── java
│ └── teprinciple
│ └── library
│ └── ExampleInstrumentedTest.java
└── main
├── AndroidManifest.xml
├── java
├── constant
│ ├── DownLoadBy.kt
│ └── UiType.kt
├── extension
│ ├── BooleanKtx.kt
│ ├── ContextKtx.kt
│ ├── CoreKtx.kt
│ └── StringKtx.kt
├── listener
│ ├── Md5CheckResultListener.kt
│ ├── OnBtnClickListener.kt
│ ├── OnInitUiListener.kt
│ └── UpdateDownloadListener.kt
├── model
│ ├── UiConfig.kt
│ ├── UpdateConfig.kt
│ └── UpdateInfo.kt
├── ui
│ └── UpdateAppActivity.kt
├── update
│ ├── DownloadAppUtils.kt
│ ├── UpdateAppReceiver.kt
│ ├── UpdateAppService.kt
│ ├── UpdateAppUtils.kt
│ └── UpdateFileProvider.kt
└── util
│ ├── AlertDialogUtil.kt
│ ├── FileDownloadUtil.kt
│ ├── GlobalContextProvider.kt
│ ├── SPUtil.kt
│ └── SignMd5Util.kt
└── res
├── anim
├── dialog_enter.xml
└── dialog_out.xml
├── drawable
├── bg_update_btn.xml
├── bg_update_dialog.xml
└── ic_update_logo.png
├── layout
├── view_update_dialog_plentiful.xml
└── view_update_dialog_simple.xml
├── values-en
└── strings.xml
├── values
├── colors.xml
├── strings.xml
└── styles.xml
└── xml
├── network_security_config.xml
└── update_file_paths.xml
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | # workflow的名称,会显示在github 的项目的Actions的右边列表中,如下图
2 | name: Android CI
3 |
4 | # 在满足以下条件触发这个workflow
5 | on:
6 | push:
7 | # 在指定的远程分支 master上,发生推送时
8 | branches: [ master ]
9 |
10 | jobs:
11 | # 多个job,如果有多个,每个以“-”开头,这里只有一个 job
12 | build:
13 |
14 | runs-on: ubuntu-latest # 该job 运行的系统环境,支持ubuntu 、windows、macOS
15 |
16 | # 下面是多个step ,每个以“-”开头
17 | steps:
18 | - uses: actions/checkout@v2 # step:检查分支
19 | - name: set up JDK 1.8 # step:设置jdk版本
20 | uses: actions/setup-java@v1 # 引用公共action
21 | with:
22 | java-version: 1.8
23 | - name: Build with Gradle # step:打包apk
24 | # 运行打包命令
25 | run: chmod +x gradlew &&./gradlew assembleRelease
26 |
27 | #step:上传apk 到action,在右上角查看
28 | # 官方文档 https://help.github.com/cn/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts#uploading-build-and-test-artifacts
29 | - name: Upload APK
30 | uses: actions/upload-artifact@v1
31 | with:
32 | name: app
33 | path: app/build/outputs/apk/release/app-release.apk
34 |
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 | .externalNativeBuild
10 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UpdateAppUtils2.0
2 |
3 | [  ](http://developer.android.com/index.html)
4 | [  ](https://bintray.com/teprinciple/maven/updateapputils/_latestVersion)
5 |
6 | ### 一行代码,快速实现app在线下载更新 A simple library for Android update app
7 |
8 | #### UpdateAppUtils2.0 特点
9 | * Kotlin First,Kotlin开发
10 | * 支持AndroidX
11 | * 支持Md5签名验证
12 | * 支持自定义任意UI
13 | * 适配中英文
14 | * 适配至Android 10
15 | * 通知栏图片自定义
16 | * 支持修改是否每次显示弹窗(非强更)
17 | * 支持安装完成后自动删除安装包
18 |
19 | UpdateAppUtils2.0功能结构变化巨大,建议使用2.0以上版本;[2.0以前版本文档](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/README_1.5.2.md)
20 |
21 | #### 效果图
22 |
23 |
24 |
25 | ### 集成
26 |
27 | ```
28 | repositories {
29 | jcenter()
30 | }
31 | ```
32 |
33 | Support
34 | ```
35 | implementation 'com.teprinciple:updateapputils:2.3.0'
36 | ```
37 |
38 | AndroidX项目
39 | ```
40 | 注意,由于操作失误bintray 中updateapputilsX被我删掉,
41 | 所以2.3.0以后使用updateapputilsx。之前的仍使用updateapputilsX
42 | //implementation 'com.teprinciple:updateapputilsX:2.2.1'
43 | implementation 'com.teprinciple:updateapputilsx:2.3.0'
44 |
45 | ```
46 |
47 | ### 使用
48 | 下面为kotlin使用示例,Java示例请参考[JavaDemo](https://github.com/teprinciple/UpdateAppUtils/blob/master/app/src/main/java/com/example/teprinciple/updateappdemo/JavaDemoActivity.java)
49 | #### 1、快速使用
50 |
51 | ##### 注意:部分手机SDK内部初始化不了context,造成context空指针,建议在application或者使用SDK前先初始化
52 | ```
53 | UpdateAppUtils.init(context)
54 | ```
55 |
56 | ```
57 | UpdateAppUtils
58 | .getInstance()
59 | .apkUrl(apkUrl)
60 | .updateTitle(updateTitle)
61 | .updateContent(updateContent)
62 | .update()
63 | ```
64 |
65 | #### 2、多配置使用
66 | ```
67 | // ui配置
68 | val uiConfig = UiConfig().apply {
69 | uiType = UiType.PLENTIFUL
70 | cancelBtnText = "下次再说"
71 | updateLogoImgRes = R.drawable.ic_update
72 | updateBtnBgRes = R.drawable.bg_btn
73 | titleTextColor = Color.BLACK
74 | titleTextSize = 18f
75 | contentTextColor = Color.parseColor("#88e16531")
76 | }
77 |
78 | // 更新配置
79 | val updateConfig = UpdateConfig().apply {
80 | force = true
81 | checkWifi = true
82 | needCheckMd5 = true
83 | isShowNotification = true
84 | notifyImgRes = R.drawable.ic_logo
85 | apkSavePath = Environment.getExternalStorageDirectory().absolutePath +"/teprinciple"
86 | apkSaveName = "teprinciple"
87 | }
88 |
89 | UpdateAppUtils
90 | .getInstance()
91 | .apkUrl(apkUrl)
92 | .updateTitle(updateTitle)
93 | .updateContent(updateContent)
94 | .updateConfig(updateConfig)
95 | .uiConfig(uiConfig)
96 | .setUpdateDownloadListener(object : UpdateDownloadListener{
97 | // do something
98 | })
99 | .update()
100 | ```
101 | #### 3、md5校验说明
102 | 为了保证app的安全性,避免apk被二次打包的风险。UpdateAppUtils内部提供了对签名文件md5值校验的接口;
103 | 首先你需要保证当前应用和服务器apk使用同一个签名文件进行了签名(一定要保管好自己的签名文件,否则检验就失去了意义),
104 | 然后你需要将UpdateConfig 的 needCheckMd5 设置为true,并在Md5CheckResultListener监听中,监听校验返回结果。具体使用可参考
105 | [CheckMd5DemoActivity](https://github.com/teprinciple/UpdateAppUtils/blob/master/app/src/main/java/com/example/teprinciple/updateappdemo/CheckMd5DemoActivity.kt)
106 | ```
107 | UpdateAppUtils
108 | .getInstance()
109 | .apkUrl(apkUrl)
110 | .updateTitle(updateTitle)
111 | .updateContent(updateContent)
112 | .updateConfig(updateConfig) // needCheckMd5设置为true
113 | .setMd5CheckResultListener(object : Md5CheckResultListener{
114 | override fun onResult(result: Boolean) {
115 | // true:检验通过,false:检验失败
116 | }
117 | })
118 | ```
119 |
120 | #### 4、自定义UI
121 | UpdateAppUtils内置了两套UI,你可以通过修改[UiConfig](#UiConfig)进行UI内容的自定义;
122 | 当然当内部UI模板与你期望UI差别很大时,你可以采用[完全自定义UI](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/%E8%87%AA%E5%AE%9A%E4%B9%89UI.md)
123 |
124 | ### Api说明
125 | #### 1、UpdateAppUtils Api
126 |
127 | | api | 说明 | 默认值 | 必须设置 |
128 | |:-------------- |:------------------------------------ |:--------------------- |:------ |
129 | | fun apkUrl(apkUrl: String)| 更新包服务器url | null | true |
130 | | fun update() | UpdateAppUtils入口 | - | true |
131 | | fun updateTitle(title: String) | 更新标题 | 版本更新啦! | false |
132 | | fun updateContent(content: String) | 更新内容 | 发现新版本,立即更新 | false |
133 | | fun updateConfig(config: UpdateConfig) | 更新配置 | 查看源码 | false |
134 | | fun uiConfig(uiConfig: UiConfig) | 更新弹窗UI配置 | 查看源码 | false |
135 | | fun setUpdateDownloadListener() | 下载过程监听 | null | false |
136 | | fun setMd5CheckResultListener() | md5校验结果回调 | null | false |
137 | | fun setOnInitUiListener() | 初始化更新弹窗UI回调 | null | false |
138 | | fun deleteInstalledApk() | 删除已安装的apk | - | false |
139 | | fun setCancelBtnClickListener() | 暂不更新按钮点击监听 | - | false |
140 | | fun setUpdateBtnClickListener() | 立即更新按钮点击监听 | - | false |
141 |
142 | #### 2、UpdateConfig:更新配置说明
143 |
144 | | 属性 | 说明 | 默认值 |
145 | |:--------------------- |:------------------------------------ |:------ |
146 | | isDebug | 是否输出【UpdateAppUtils】为Tag的日志| true |
147 | | force | 是否强制更新,强制时无取消按钮 | false |
148 | | apkSavePath | apk下载存放位置 | 包名目录 |
149 | | apkSaveName | apk保存文件名 | 项目名 |
150 | | downloadBy | 下载方式 | DownLoadBy.APP |
151 | | needCheckMd5 | 是否需要校验apk签名md5 | false |
152 | | checkWifi | 检查是否wifi | false |
153 | | isShowNotification | 是否显示通知栏进度 | true |
154 | | notifyImgRes | 通知栏图标 | 项目icon |
155 | | serverVersionName | 服务器上apk版本名 | 无 |
156 | | serverVersionCode | 服务器上apk版本号 | 无 |
157 | | alwaysShow | 是否每次显示更新弹窗(非强更) | true |
158 | | thisTimeShow | 本次是否显示更新弹窗(非强更) | false |
159 | | alwaysShowDownLoadDialog| 是否需要显示更新下载进度弹窗(非强更) | false |
160 | | showDownloadingToast | 开始下载时是否显示Toast | true |
161 |
162 | #### 3、UiConfig:更新弹窗Ui配置说明
163 |
164 | | 属性 | 说明 | 默认值 |
165 | |:--------------------- |:------------------------------------ |:------ |
166 | | uiType | ui模板 | UiType.SIMPLE |
167 | | customLayoutId | 自定义布局id | false |
168 | | updateLogoImgRes | 更新弹窗logo图片资源id | - |
169 | | titleTextSize | 标题字体大小 | 16sp |
170 | | titleTextColor | 标题字体颜色 | - |
171 | | contentTextSize | 内容字体大小 | 14sp |
172 | | contentTextColor | 内容字体颜色 | - |
173 | | updateBtnBgColor | 更新按钮背景颜色 | - |
174 | | updateBtnBgRes | 更新按钮背景资源id | - |
175 | | updateBtnTextColor | 更新按钮字体颜色 | - |
176 | | updateBtnTextSize | 更新按钮文字大小 | 14sp |
177 | | updateBtnText | 更新按钮文案 | 立即更新 |
178 | | cancelBtnBgColor | 取消按钮背景颜色 | - |
179 | | cancelBtnBgRes | 取消按钮背景资源id | - |
180 | | cancelBtnTextColor | 取消按钮文字颜色 | - |
181 | | cancelBtnTextSize | 取消按钮文字大小 | 14sp |
182 | | cancelBtnText | 取消按钮文案 | 暂不更新 |
183 | | downloadingToastText | 开始下载时的Toast提示文字 | 更新下载中... |
184 | | downloadingBtnText | 下载中 下载按钮以及通知栏标题前缀,进度自动拼接在后面 | 下载中 |
185 | | downloadFailText | 下载出错时,下载按钮及通知栏标题 | 下载出错,点击重试 |
186 |
187 | ### Demo体验
188 |
189 |
190 | ### 更新日志
191 |
192 | #### 2.3.0
193 | * 修复部分手机context空指针异常
194 | ##### [更多历史版本](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/version.md)
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-android-extensions'
4 |
5 | android {
6 | compileSdkVersion 29
7 | defaultConfig {
8 | applicationId "com.example.teprinciple.updateappdemo"
9 | minSdkVersion 19
10 | targetSdkVersion 29
11 | versionCode 203
12 | versionName "2.0.3"
13 | }
14 | signingConfigs {
15 | config {
16 | storeFile file('../update.jks')
17 | storePassword '123456'
18 | keyAlias 'update'
19 | keyPassword '123456'
20 | }
21 | }
22 |
23 | buildTypes {
24 | release {
25 | minifyEnabled false
26 | signingConfig signingConfigs.debug
27 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
28 | }
29 |
30 | debug {
31 | minifyEnabled false
32 | signingConfig signingConfigs.config
33 | }
34 | }
35 |
36 | lintOptions {
37 | abortOnError false
38 | }
39 | }
40 |
41 | dependencies {
42 | implementation fileTree(include: ['*.jar'], dir: 'libs')
43 | implementation project(':updateapputils')
44 | implementation 'com.android.support:appcompat-v7:28.0.0'
45 | implementation 'com.android.support.constraint:constraint-layout:1.1.3'
46 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
47 |
48 | debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.1'
49 | }
50 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in C:\Users\Teprinciple\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/example/teprinciple/updateappdemo/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package com.example.teprinciple.updateappdemo;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("com.example.teprinciple.updateappdemo", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/teprinciple/updateappdemo/CheckMd5DemoActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.teprinciple.updateappdemo
2 |
3 | import android.os.Bundle
4 | import android.os.Environment
5 | import android.support.v7.app.AppCompatActivity
6 | import android.widget.Toast
7 | import kotlinx.android.synthetic.main.check_md5_demo_activity.*
8 | import listener.Md5CheckResultListener
9 | import model.UpdateConfig
10 | import update.UpdateAppUtils
11 |
12 | /**
13 | * desc: md5校验示例
14 | * time: 2019/7/1
15 | * @author yk
16 | */
17 | class CheckMd5DemoActivity : AppCompatActivity() {
18 |
19 | /**
20 | * 已签名的apk
21 | */
22 | private val signedApkUrl = "http://118.24.148.250:8080/yk/update_signed.apk"
23 |
24 | /**
25 | * 非正规签名的apk
26 | */
27 | private val notSignedApkUrl = "http://118.24.148.250:8080/yk/update_not_signed.apk"
28 |
29 | private val updateTitle = "发现新版本V2.0.0"
30 | private val updateContent = "1、Kotlin重构版\n2、支持自定义UI\n3、增加md5校验\n4、更多功能等你探索"
31 |
32 | override fun onCreate(savedInstanceState: Bundle?) {
33 | super.onCreate(savedInstanceState)
34 |
35 | setContentView(R.layout.check_md5_demo_activity)
36 |
37 | // 更新配置
38 | val updateConfig = UpdateConfig().apply {
39 | force = true
40 | needCheckMd5 = true
41 | }
42 |
43 | // 正确签名
44 | btn_signed.setOnClickListener {
45 | updateConfig.apply { apkSaveName = "signed" }
46 | UpdateAppUtils
47 | .getInstance()
48 | .apkUrl(signedApkUrl)
49 | .updateTitle(updateTitle)
50 | .updateContent(updateContent)
51 | .updateConfig(updateConfig)
52 | .setMd5CheckResultListener(object : Md5CheckResultListener {
53 | override fun onResult(result: Boolean) {
54 | Toast.makeText(this@CheckMd5DemoActivity, "Md5检验是否通过:$result", Toast.LENGTH_SHORT).show()
55 | }
56 | })
57 | .update()
58 | }
59 |
60 | // 错误签名
61 | btn_not_signed.setOnClickListener {
62 | updateConfig.apply { apkSaveName = "not_signed" }
63 | UpdateAppUtils
64 | .getInstance()
65 | .apkUrl(notSignedApkUrl)
66 | .updateTitle(updateTitle)
67 | .updateContent(updateContent)
68 | .updateConfig(updateConfig)
69 | .setMd5CheckResultListener(object : Md5CheckResultListener {
70 | override fun onResult(result: Boolean) {
71 | Toast.makeText(this@CheckMd5DemoActivity, "Md5检验是否通过:$result", Toast.LENGTH_SHORT).show()
72 | }
73 | })
74 | .update()
75 | }
76 | }
77 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/teprinciple/updateappdemo/JavaDemoActivity.java:
--------------------------------------------------------------------------------
1 | package com.example.teprinciple.updateappdemo;
2 |
3 | import android.os.Bundle;
4 | import android.support.annotation.Nullable;
5 | import android.support.v7.app.AppCompatActivity;
6 | import android.view.View;
7 |
8 | import org.jetbrains.annotations.NotNull;
9 |
10 | import constant.UiType;
11 | import listener.Md5CheckResultListener;
12 | import listener.UpdateDownloadListener;
13 | import model.UiConfig;
14 | import model.UpdateConfig;
15 | import update.UpdateAppUtils;
16 |
17 | /**
18 | * desc: java使用实例
19 | * time: 2019/6/27
20 | * @author yk
21 | */
22 | public class JavaDemoActivity extends AppCompatActivity {
23 |
24 | private String apkUrl = "http://118.24.148.250:8080/yk/update_signed.apk";
25 | private String updateTitle = "发现新版本V2.0.0";
26 | private String updateContent = "1、Kotlin重构版\n2、支持自定义UI\n3、增加md5校验\n4、更多功能等你探索";
27 |
28 | @Override
29 | protected void onCreate(@Nullable Bundle savedInstanceState) {
30 | super.onCreate(savedInstanceState);
31 | setContentView(R.layout.activity_java_demo);
32 |
33 | UpdateAppUtils.init(this);
34 |
35 | findViewById(R.id.btn_java).setOnClickListener(new View.OnClickListener() {
36 | @Override
37 | public void onClick(View v) {
38 |
39 | UpdateConfig updateConfig = new UpdateConfig();
40 | updateConfig.setCheckWifi(true);
41 | updateConfig.setNeedCheckMd5(true);
42 | updateConfig.setNotifyImgRes(R.drawable.ic_logo);
43 |
44 | UiConfig uiConfig = new UiConfig();
45 | uiConfig.setUiType(UiType.PLENTIFUL);
46 |
47 | UpdateAppUtils
48 | .getInstance()
49 | .apkUrl(apkUrl)
50 | .updateTitle(updateTitle)
51 | .updateContent(updateContent)
52 | .uiConfig(uiConfig)
53 | .updateConfig(updateConfig)
54 | .setMd5CheckResultListener(new Md5CheckResultListener() {
55 | @Override
56 | public void onResult(boolean result) {
57 |
58 | }
59 | })
60 | .setUpdateDownloadListener(new UpdateDownloadListener() {
61 | @Override
62 | public void onStart() {
63 |
64 | }
65 |
66 | @Override
67 | public void onDownload(int progress) {
68 |
69 | }
70 |
71 | @Override
72 | public void onFinish() {
73 |
74 | }
75 |
76 | @Override
77 | public void onError(@NotNull Throwable e) {
78 |
79 | }
80 | })
81 | .update();
82 | }
83 | });
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/teprinciple/updateappdemo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.teprinciple.updateappdemo
2 |
3 | import android.content.Intent
4 | import android.graphics.Color
5 | import android.os.Bundle
6 | import android.os.Environment
7 | import android.support.v7.app.AppCompatActivity
8 | import android.view.View
9 | import android.widget.TextView
10 | import android.widget.Toast
11 | import constant.DownLoadBy
12 | import constant.UiType
13 | import kotlinx.android.synthetic.main.activity_main.*
14 | import listener.OnBtnClickListener
15 | import listener.OnInitUiListener
16 | import listener.UpdateDownloadListener
17 | import model.UiConfig
18 | import model.UpdateConfig
19 | import update.UpdateAppUtils
20 |
21 |
22 | class MainActivity : AppCompatActivity() {
23 |
24 | private val apkUrl = "http://118.24.148.250:8080/yk/update_signed.apk"
25 | // private val apkUrl = "https://github.com/AlexLiuSheng/CheckVersionLib/blob/master/library/src/main/java/com/allenliu/versionchecklib/utils/AppUtils.java"
26 | private val updateTitle = "发现新版本V2.0.0"
27 | private val updateContent = "1、Kotlin重构版\n2、支持自定义UI\n3、增加md5校验\n4、更多功能等你探索"
28 |
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 | setContentView(R.layout.activity_main)
32 |
33 | UpdateAppUtils.init(this)
34 |
35 | // 基本使用
36 | btn_basic_use.setOnClickListener {
37 | UpdateAppUtils
38 | .getInstance()
39 | .apkUrl(apkUrl)
40 | .updateTitle(updateTitle)
41 | .updateConfig(UpdateConfig(apkSaveName = "up_1.1"))
42 | .uiConfig(UiConfig(uiType = UiType.SIMPLE))
43 | .updateContent(updateContent)
44 | .update()
45 | }
46 |
47 | // 浏览器下载
48 | btn_download_by_browser.setOnClickListener {
49 |
50 | // 使用SpannableString
51 | val content = SpanUtils(this)
52 | .appendLine("1、Kotlin重构版")
53 | .appendLine("2、支持自定义UI").setForegroundColor(Color.RED)
54 | .appendLine("3、增加md5校验").setForegroundColor(Color.parseColor("#88e16531")).setFontSize(20, true)
55 | .appendLine("4、更多功能等你探索").setBoldItalic()
56 | .appendLine().appendImage(R.mipmap.ic_launcher).setBoldItalic()
57 | .create()
58 |
59 | UpdateAppUtils
60 | .getInstance()
61 | .apkUrl(apkUrl)
62 | .updateTitle(updateTitle)
63 | .updateContent(content)
64 | .updateConfig(UpdateConfig().apply {
65 | downloadBy = DownLoadBy.BROWSER
66 | // alwaysShow = false
67 | serverVersionName = "2.0.0"
68 | })
69 | .uiConfig(UiConfig(uiType = UiType.PLENTIFUL))
70 |
71 | // 设置 取消 按钮点击事件
72 | .setCancelBtnClickListener(object : OnBtnClickListener {
73 | override fun onClick(): Boolean {
74 | Toast.makeText(this@MainActivity, "cancel btn click", Toast.LENGTH_SHORT).show()
75 | return false // 事件是否消费,是否需要传递下去。false-会执行原有点击逻辑,true-只执行本次设置的点击逻辑
76 | }
77 | })
78 |
79 | // 设置 立即更新 按钮点击事件
80 | .setUpdateBtnClickListener(object : OnBtnClickListener {
81 | override fun onClick(): Boolean {
82 | Toast.makeText(this@MainActivity, "update btn click", Toast.LENGTH_SHORT).show()
83 | return false // 事件是否消费,是否需要传递下去。false-会执行原有点击逻辑,true-只执行本次设置的点击逻辑
84 | }
85 | })
86 |
87 | .update()
88 | }
89 |
90 | // 自定义UI
91 | btn_custom_ui.setOnClickListener {
92 | UpdateAppUtils
93 | .getInstance()
94 | .apkUrl(apkUrl)
95 | .updateTitle(updateTitle)
96 | .updateContent(updateContent)
97 | .updateConfig(UpdateConfig(alwaysShowDownLoadDialog = true))
98 | .uiConfig(UiConfig(uiType = UiType.CUSTOM, customLayoutId = R.layout.view_update_dialog_custom))
99 | .setOnInitUiListener(object : OnInitUiListener {
100 | override fun onInitUpdateUi(view: View?, updateConfig: UpdateConfig, uiConfig: UiConfig) {
101 | view?.findViewById(R.id.tv_update_title)?.text = "版本更新啦"
102 | view?.findViewById(R.id.tv_version_name)?.text = "V2.0.0"
103 | // do more...
104 | }
105 | })
106 | .update()
107 | }
108 |
109 | // java使用示例
110 | btn_java_sample.setOnClickListener {
111 | startActivity(Intent(this, JavaDemoActivity::class.java))
112 | }
113 |
114 | // md5校验
115 | btn_check_md5.setOnClickListener {
116 | startActivity(Intent(this, CheckMd5DemoActivity::class.java))
117 | }
118 |
119 | // 高级使用
120 | btn_higher_level_use.setOnClickListener {
121 | // ui配置
122 | val uiConfig = UiConfig().apply {
123 | uiType = UiType.PLENTIFUL
124 | cancelBtnText = "下次再说"
125 | updateLogoImgRes = R.drawable.ic_update
126 | updateBtnBgRes = R.drawable.bg_btn
127 | titleTextColor = Color.BLACK
128 | titleTextSize = 18f
129 | contentTextColor = Color.parseColor("#88e16531")
130 | }
131 |
132 | // 更新配置
133 | val updateConfig = UpdateConfig().apply {
134 | force = true
135 | isDebug = true
136 | checkWifi = true
137 | isShowNotification = true
138 | notifyImgRes = R.drawable.ic_logo
139 | apkSavePath = Environment.getExternalStorageDirectory().absolutePath + "/teprinciple"
140 | apkSaveName = "teprinciple"
141 | }
142 |
143 | UpdateAppUtils
144 | .getInstance()
145 | .apkUrl(apkUrl)
146 | .updateTitle(updateTitle)
147 | .updateContent(updateContent)
148 | .updateConfig(updateConfig)
149 | .uiConfig(uiConfig)
150 | .setUpdateDownloadListener(object : UpdateDownloadListener {
151 | override fun onStart() {
152 | }
153 |
154 | override fun onDownload(progress: Int) {
155 | }
156 |
157 | override fun onFinish() {
158 | }
159 |
160 | override fun onError(e: Throwable) {
161 | }
162 | })
163 | .update()
164 | }
165 | }
166 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/teprinciple/updateappdemo/SpanUtils.java:
--------------------------------------------------------------------------------
1 | package com.example.teprinciple.updateappdemo;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.content.Context;
5 | import android.graphics.Bitmap;
6 | import android.graphics.BitmapFactory;
7 | import android.graphics.BlurMaskFilter;
8 | import android.graphics.Canvas;
9 | import android.graphics.Color;
10 | import android.graphics.Paint;
11 | import android.graphics.Path;
12 | import android.graphics.Rect;
13 | import android.graphics.Shader;
14 | import android.graphics.Typeface;
15 | import android.graphics.drawable.BitmapDrawable;
16 | import android.graphics.drawable.Drawable;
17 | import android.net.Uri;
18 | import android.support.annotation.ColorInt;
19 | import android.support.annotation.DrawableRes;
20 | import android.support.annotation.FloatRange;
21 | import android.support.annotation.IntDef;
22 | import android.support.annotation.IntRange;
23 | import android.support.annotation.NonNull;
24 | import android.support.annotation.Nullable;
25 | import android.support.v4.content.ContextCompat;
26 | import android.text.Layout;
27 | import android.text.Layout.Alignment;
28 | import android.text.SpannableStringBuilder;
29 | import android.text.Spanned;
30 | import android.text.TextPaint;
31 | import android.text.style.AbsoluteSizeSpan;
32 | import android.text.style.AlignmentSpan;
33 | import android.text.style.BackgroundColorSpan;
34 | import android.text.style.CharacterStyle;
35 | import android.text.style.ClickableSpan;
36 | import android.text.style.ForegroundColorSpan;
37 | import android.text.style.LeadingMarginSpan;
38 | import android.text.style.LineHeightSpan;
39 | import android.text.style.MaskFilterSpan;
40 | import android.text.style.RelativeSizeSpan;
41 | import android.text.style.ReplacementSpan;
42 | import android.text.style.ScaleXSpan;
43 | import android.text.style.StrikethroughSpan;
44 | import android.text.style.StyleSpan;
45 | import android.text.style.SubscriptSpan;
46 | import android.text.style.SuperscriptSpan;
47 | import android.text.style.TypefaceSpan;
48 | import android.text.style.URLSpan;
49 | import android.text.style.UnderlineSpan;
50 | import android.text.style.UpdateAppearance;
51 | import android.util.Log;
52 |
53 | import java.io.InputStream;
54 | import java.lang.annotation.Retention;
55 | import java.lang.annotation.RetentionPolicy;
56 | import java.lang.ref.WeakReference;
57 |
58 | import static android.graphics.BlurMaskFilter.Blur;
59 |
60 | /**
61 | *
62 | * author: Blankj
63 | * blog : http://blankj.com
64 | * usage : https://www.jianshu.com/p/509b0d2626f4
65 | * time : 16/12/13
66 | * desc : utils about span
67 | *
68 | */
69 | public final class SpanUtils {
70 |
71 | private static final int COLOR_DEFAULT = 0xFEFFFFFF;
72 |
73 | public static final int ALIGN_BOTTOM = 0;
74 | public static final int ALIGN_BASELINE = 1;
75 | public static final int ALIGN_CENTER = 2;
76 | public static final int ALIGN_TOP = 3;
77 |
78 | @IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER, ALIGN_TOP})
79 | @Retention(RetentionPolicy.SOURCE)
80 | public @interface Align {
81 | }
82 |
83 | private static final String LINE_SEPARATOR = System.getProperty("line.separator");
84 |
85 | private CharSequence mText;
86 | private int flag;
87 | private int foregroundColor;
88 | private int backgroundColor;
89 | private int lineHeight;
90 | private int alignLine;
91 | private int quoteColor;
92 | private int stripeWidth;
93 | private int quoteGapWidth;
94 | private int first;
95 | private int rest;
96 | private int bulletColor;
97 | private int bulletRadius;
98 | private int bulletGapWidth;
99 | private int fontSize;
100 | private boolean fontSizeIsDp;
101 | private float proportion;
102 | private float xProportion;
103 | private boolean isStrikethrough;
104 | private boolean isUnderline;
105 | private boolean isSuperscript;
106 | private boolean isSubscript;
107 | private boolean isBold;
108 | private boolean isItalic;
109 | private boolean isBoldItalic;
110 | private String fontFamily;
111 | private Typeface typeface;
112 | private Alignment alignment;
113 | private ClickableSpan clickSpan;
114 | private String url;
115 | private float blurRadius;
116 | private Blur style;
117 | private Shader shader;
118 | private float shadowRadius;
119 | private float shadowDx;
120 | private float shadowDy;
121 | private int shadowColor;
122 | private Object[] spans;
123 |
124 | private Bitmap imageBitmap;
125 | private Drawable imageDrawable;
126 | private Uri imageUri;
127 | private int imageResourceId;
128 | private int alignImage;
129 |
130 | private int spaceSize;
131 | private int spaceColor;
132 |
133 | private SpannableStringBuilder mBuilder;
134 |
135 | private int mType;
136 | private final int mTypeCharSequence = 0;
137 | private final int mTypeImage = 1;
138 | private final int mTypeSpace = 2;
139 |
140 | private Context mContext;
141 |
142 | public SpanUtils(Context context) {
143 | mContext = context.getApplicationContext();
144 | mBuilder = new SpannableStringBuilder();
145 | mText = "";
146 | setDefault();
147 | }
148 |
149 | private void setDefault() {
150 | flag = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
151 | foregroundColor = COLOR_DEFAULT;
152 | backgroundColor = COLOR_DEFAULT;
153 | lineHeight = -1;
154 | quoteColor = COLOR_DEFAULT;
155 | first = -1;
156 | bulletColor = COLOR_DEFAULT;
157 | fontSize = -1;
158 | proportion = -1;
159 | xProportion = -1;
160 | isStrikethrough = false;
161 | isUnderline = false;
162 | isSuperscript = false;
163 | isSubscript = false;
164 | isBold = false;
165 | isItalic = false;
166 | isBoldItalic = false;
167 | fontFamily = null;
168 | typeface = null;
169 | alignment = null;
170 | clickSpan = null;
171 | url = null;
172 | blurRadius = -1;
173 | shader = null;
174 | shadowRadius = -1;
175 | spans = null;
176 |
177 | imageBitmap = null;
178 | imageDrawable = null;
179 | imageUri = null;
180 | imageResourceId = -1;
181 |
182 | spaceSize = -1;
183 | }
184 |
185 | /**
186 | * Set the span of flag.
187 | *
188 | * @param flag
189 | * The flag.
190 | *
191 | * - {@link Spanned#SPAN_INCLUSIVE_EXCLUSIVE}
192 | * - {@link Spanned#SPAN_INCLUSIVE_INCLUSIVE}
193 | * - {@link Spanned#SPAN_EXCLUSIVE_EXCLUSIVE}
194 | * - {@link Spanned#SPAN_EXCLUSIVE_INCLUSIVE}
195 | *
196 | * @return the single {@link SpanUtils} instance
197 | */
198 | public SpanUtils setFlag(final int flag) {
199 | this.flag = flag;
200 | return this;
201 | }
202 |
203 | /**
204 | * Set the span of foreground's color.
205 | *
206 | * @param color
207 | * The color of foreground
208 | * @return the single {@link SpanUtils} instance
209 | */
210 | public SpanUtils setForegroundColor(@ColorInt final int color) {
211 | this.foregroundColor = color;
212 | return this;
213 | }
214 |
215 | /**
216 | * Set the span of background's color.
217 | *
218 | * @param color
219 | * The color of background
220 | * @return the single {@link SpanUtils} instance
221 | */
222 | public SpanUtils setBackgroundColor(@ColorInt final int color) {
223 | this.backgroundColor = color;
224 | return this;
225 | }
226 |
227 | /**
228 | * Set the span of line height.
229 | *
230 | * @param lineHeight
231 | * The line height, in pixel.
232 | * @return the single {@link SpanUtils} instance
233 | */
234 | public SpanUtils setLineHeight(@IntRange(from = 0) final int lineHeight) {
235 | return setLineHeight(lineHeight, ALIGN_CENTER);
236 | }
237 |
238 | /**
239 | * Set the span of line height.
240 | *
241 | * @param lineHeight
242 | * The line height, in pixel.
243 | * @param align
244 | * The alignment.
245 | *
246 | * - {@link Align#ALIGN_TOP }
247 | * - {@link Align#ALIGN_CENTER}
248 | * - {@link Align#ALIGN_BOTTOM}
249 | *
250 | * @return the single {@link SpanUtils} instance
251 | */
252 | public SpanUtils setLineHeight(@IntRange(from = 0) final int lineHeight,
253 | @Align final int align) {
254 | this.lineHeight = lineHeight;
255 | this.alignLine = align;
256 | return this;
257 | }
258 |
259 | /**
260 | * Set the span of quote's color.
261 | *
262 | * @param color
263 | * The color of quote
264 | * @return the single {@link SpanUtils} instance
265 | */
266 | public SpanUtils setQuoteColor(@ColorInt final int color) {
267 | return setQuoteColor(color, 2, 2);
268 | }
269 |
270 | /**
271 | * Set the span of quote's color.
272 | *
273 | * @param color
274 | * The color of quote.
275 | * @param stripeWidth
276 | * The width of stripe, in pixel.
277 | * @param gapWidth
278 | * The width of gap, in pixel.
279 | * @return the single {@link SpanUtils} instance
280 | */
281 | public SpanUtils setQuoteColor(@ColorInt final int color,
282 | @IntRange(from = 1) final int stripeWidth,
283 | @IntRange(from = 0) final int gapWidth) {
284 | this.quoteColor = color;
285 | this.stripeWidth = stripeWidth;
286 | this.quoteGapWidth = gapWidth;
287 | return this;
288 | }
289 |
290 | /**
291 | * Set the span of leading margin.
292 | *
293 | * @param first
294 | * The indent for the first line of the paragraph.
295 | * @param rest
296 | * The indent for the remaining lines of the paragraph.
297 | * @return the single {@link SpanUtils} instance
298 | */
299 | public SpanUtils setLeadingMargin(@IntRange(from = 0) final int first,
300 | @IntRange(from = 0) final int rest) {
301 | this.first = first;
302 | this.rest = rest;
303 | return this;
304 | }
305 |
306 | /**
307 | * Set the span of bullet.
308 | *
309 | * @param gapWidth
310 | * The width of gap, in pixel.
311 | * @return the single {@link SpanUtils} instance
312 | */
313 | public SpanUtils setBullet(@IntRange(from = 0) final int gapWidth) {
314 | return setBullet(0, 3, gapWidth);
315 | }
316 |
317 | /**
318 | * Set the span of bullet.
319 | *
320 | * @param color
321 | * The color of bullet.
322 | * @param radius
323 | * The radius of bullet, in pixel.
324 | * @param gapWidth
325 | * The width of gap, in pixel.
326 | * @return the single {@link SpanUtils} instance
327 | */
328 | public SpanUtils setBullet(@ColorInt final int color,
329 | @IntRange(from = 0) final int radius,
330 | @IntRange(from = 0) final int gapWidth) {
331 | this.bulletColor = color;
332 | this.bulletRadius = radius;
333 | this.bulletGapWidth = gapWidth;
334 | return this;
335 | }
336 |
337 | /**
338 | * Set the span of font's size.
339 | *
340 | * @param size
341 | * The size of font.
342 | * @return the single {@link SpanUtils} instance
343 | */
344 | public SpanUtils setFontSize(@IntRange(from = 0) final int size) {
345 | return setFontSize(size, false);
346 | }
347 |
348 | /**
349 | * Set the span of size of font.
350 | *
351 | * @param size
352 | * The size of font.
353 | * @param isSp
354 | * True to use sp, false to use pixel.
355 | * @return the single {@link SpanUtils} instance
356 | */
357 | public SpanUtils setFontSize(@IntRange(from = 0) final int size, final boolean isSp) {
358 | this.fontSize = size;
359 | this.fontSizeIsDp = isSp;
360 | return this;
361 | }
362 |
363 | /**
364 | * Set the span of proportion of font.
365 | *
366 | * @param proportion
367 | * The proportion of font.
368 | * @return the single {@link SpanUtils} instance
369 | */
370 | public SpanUtils setFontProportion(final float proportion) {
371 | this.proportion = proportion;
372 | return this;
373 | }
374 |
375 | /**
376 | * Set the span of transverse proportion of font.
377 | *
378 | * @param proportion
379 | * The transverse proportion of font.
380 | * @return the single {@link SpanUtils} instance
381 | */
382 | public SpanUtils setFontXProportion(final float proportion) {
383 | this.xProportion = proportion;
384 | return this;
385 | }
386 |
387 | /**
388 | * Set the span of strikethrough.
389 | *
390 | * @return the single {@link SpanUtils} instance
391 | */
392 | public SpanUtils setStrikethrough() {
393 | this.isStrikethrough = true;
394 | return this;
395 | }
396 |
397 | /**
398 | * Set the span of underline.
399 | *
400 | * @return the single {@link SpanUtils} instance
401 | */
402 | public SpanUtils setUnderline() {
403 | this.isUnderline = true;
404 | return this;
405 | }
406 |
407 | /**
408 | * Set the span of superscript.
409 | *
410 | * @return the single {@link SpanUtils} instance
411 | */
412 | public SpanUtils setSuperscript() {
413 | this.isSuperscript = true;
414 | return this;
415 | }
416 |
417 | /**
418 | * Set the span of subscript.
419 | *
420 | * @return the single {@link SpanUtils} instance
421 | */
422 | public SpanUtils setSubscript() {
423 | this.isSubscript = true;
424 | return this;
425 | }
426 |
427 | /**
428 | * Set the span of bold.
429 | *
430 | * @return the single {@link SpanUtils} instance
431 | */
432 | public SpanUtils setBold() {
433 | isBold = true;
434 | return this;
435 | }
436 |
437 | /**
438 | * Set the span of bold.
439 | *
440 | * @return the single {@link SpanUtils} instance
441 | */
442 | public SpanUtils setNotBold() {
443 | isBold = false;
444 | return this;
445 | }
446 |
447 | /**
448 | * Set the span of italic.
449 | *
450 | * @return the single {@link SpanUtils} instance
451 | */
452 | public SpanUtils setItalic() {
453 | isItalic = true;
454 | return this;
455 | }
456 |
457 | /**
458 | * Set the span of bold italic.
459 | *
460 | * @return the single {@link SpanUtils} instance
461 | */
462 | public SpanUtils setBoldItalic() {
463 | isBoldItalic = true;
464 | return this;
465 | }
466 |
467 | /**
468 | * Set the span of font family.
469 | *
470 | * @param fontFamily
471 | * The font family.
472 | *
473 | * - monospace
474 | * - serif
475 | * - sans-serif
476 | *
477 | * @return the single {@link SpanUtils} instance
478 | */
479 | public SpanUtils setFontFamily(@NonNull final String fontFamily) {
480 | this.fontFamily = fontFamily;
481 | return this;
482 | }
483 |
484 | /**
485 | * Set the span of typeface.
486 | *
487 | * @param typeface
488 | * The typeface.
489 | * @return the single {@link SpanUtils} instance
490 | */
491 | public SpanUtils setTypeface(@NonNull final Typeface typeface) {
492 | this.typeface = typeface;
493 | return this;
494 | }
495 |
496 | /**
497 | * Set the span of alignment.
498 | *
499 | * @param alignment
500 | * The alignment.
501 | *
502 | * - {@link Alignment#ALIGN_NORMAL }
503 | * - {@link Alignment#ALIGN_OPPOSITE}
504 | * - {@link Alignment#ALIGN_CENTER }
505 | *
506 | * @return the single {@link SpanUtils} instance
507 | */
508 | public SpanUtils setAlign(@NonNull final Alignment alignment) {
509 | this.alignment = alignment;
510 | return this;
511 | }
512 |
513 | /**
514 | * Set the span of click.
515 | * Must set {@code view.setMovementMethod(LinkMovementMethod.getInstance())}
516 | *
517 | * @param clickSpan
518 | * The span of click.
519 | * @return the single {@link SpanUtils} instance
520 | */
521 | public SpanUtils setClickSpan(@NonNull final ClickableSpan clickSpan) {
522 | this.clickSpan = clickSpan;
523 | return this;
524 | }
525 |
526 | /**
527 | * Set the span of url.
528 | * Must set {@code view.setMovementMethod(LinkMovementMethod.getInstance())}
529 | *
530 | * @param url
531 | * The url.
532 | * @return the single {@link SpanUtils} instance
533 | */
534 | public SpanUtils setUrl(@NonNull final String url) {
535 | this.url = url;
536 | return this;
537 | }
538 |
539 | /**
540 | * Set the span of blur.
541 | *
542 | * @param radius
543 | * The radius of blur.
544 | * @param style
545 | * The style.
546 | *
547 | * - {@link Blur#NORMAL}
548 | * - {@link Blur#SOLID}
549 | * - {@link Blur#OUTER}
550 | * - {@link Blur#INNER}
551 | *
552 | * @return the single {@link SpanUtils} instance
553 | */
554 | public SpanUtils setBlur(@FloatRange(from = 0, fromInclusive = false) final float radius,
555 | final Blur style) {
556 | this.blurRadius = radius;
557 | this.style = style;
558 | return this;
559 | }
560 |
561 | /**
562 | * Set the span of shader.
563 | *
564 | * @param shader
565 | * The shader.
566 | * @return the single {@link SpanUtils} instance
567 | */
568 | public SpanUtils setShader(@NonNull final Shader shader) {
569 | this.shader = shader;
570 | return this;
571 | }
572 |
573 | /**
574 | * Set the span of shadow.
575 | *
576 | * @param radius
577 | * The radius of shadow.
578 | * @param dx
579 | * X-axis offset, in pixel.
580 | * @param dy
581 | * Y-axis offset, in pixel.
582 | * @param shadowColor
583 | * The color of shadow.
584 | * @return the single {@link SpanUtils} instance
585 | */
586 | public SpanUtils setShadow(@FloatRange(from = 0, fromInclusive = false) final float radius,
587 | final float dx,
588 | final float dy,
589 | final int shadowColor) {
590 | this.shadowRadius = radius;
591 | this.shadowDx = dx;
592 | this.shadowDy = dy;
593 | this.shadowColor = shadowColor;
594 | return this;
595 | }
596 |
597 |
598 | /**
599 | * Set the spans.
600 | *
601 | * @param spans
602 | * The spans.
603 | * @return the single {@link SpanUtils} instance
604 | */
605 | public SpanUtils setSpans(@NonNull final Object... spans) {
606 | if (spans.length > 0) {
607 | this.spans = spans;
608 | }
609 | return this;
610 | }
611 |
612 | /**
613 | * Append the text text.
614 | *
615 | * @param text
616 | * The text.
617 | * @return the single {@link SpanUtils} instance
618 | */
619 | public SpanUtils append(@NonNull final CharSequence text) {
620 | apply(mTypeCharSequence);
621 | mText = text;
622 | return this;
623 | }
624 |
625 | /**
626 | * Append one line.
627 | *
628 | * @return the single {@link SpanUtils} instance
629 | */
630 | public SpanUtils appendLine() {
631 | apply(mTypeCharSequence);
632 | mText = LINE_SEPARATOR;
633 | return this;
634 | }
635 |
636 | /**
637 | * Append text and one line.
638 | *
639 | * @return the single {@link SpanUtils} instance
640 | */
641 | public SpanUtils appendLine(@NonNull final CharSequence text) {
642 | apply(mTypeCharSequence);
643 | mText = text + LINE_SEPARATOR;
644 | return this;
645 | }
646 |
647 | /**
648 | * Append one image.
649 | *
650 | * @param bitmap
651 | * The bitmap of image.
652 | * @return the single {@link SpanUtils} instance
653 | */
654 | public SpanUtils appendImage(@NonNull final Bitmap bitmap) {
655 | return appendImage(bitmap, ALIGN_BOTTOM);
656 | }
657 |
658 | /**
659 | * Append one image.
660 | *
661 | * @param bitmap
662 | * The bitmap.
663 | * @param align
664 | * The alignment.
665 | *
666 | * - {@link Align#ALIGN_TOP }
667 | * - {@link Align#ALIGN_CENTER }
668 | * - {@link Align#ALIGN_BASELINE}
669 | * - {@link Align#ALIGN_BOTTOM }
670 | *
671 | * @return the single {@link SpanUtils} instance
672 | */
673 | public SpanUtils appendImage(@NonNull final Bitmap bitmap, @Align final int align) {
674 | apply(mTypeImage);
675 | this.imageBitmap = bitmap;
676 | this.alignImage = align;
677 | return this;
678 | }
679 |
680 | /**
681 | * Append one image.
682 | *
683 | * @param drawable
684 | * The drawable of image.
685 | * @return the single {@link SpanUtils} instance
686 | */
687 | public SpanUtils appendImage(@NonNull final Drawable drawable) {
688 | return appendImage(drawable, ALIGN_BOTTOM);
689 | }
690 |
691 | /**
692 | * Append one image.
693 | *
694 | * @param drawable
695 | * The drawable of image.
696 | * @param align
697 | * The alignment.
698 | *
699 | * - {@link Align#ALIGN_TOP }
700 | * - {@link Align#ALIGN_CENTER }
701 | * - {@link Align#ALIGN_BASELINE}
702 | * - {@link Align#ALIGN_BOTTOM }
703 | *
704 | * @return the single {@link SpanUtils} instance
705 | */
706 | public SpanUtils appendImage(@NonNull final Drawable drawable, @Align final int align) {
707 | apply(mTypeImage);
708 | this.imageDrawable = drawable;
709 | this.alignImage = align;
710 | return this;
711 | }
712 |
713 | /**
714 | * Append one image.
715 | *
716 | * @param uri
717 | * The uri of image.
718 | * @return the single {@link SpanUtils} instance
719 | */
720 | public SpanUtils appendImage(@NonNull final Uri uri) {
721 | return appendImage(uri, ALIGN_BOTTOM);
722 | }
723 |
724 | /**
725 | * Append one image.
726 | *
727 | * @param uri
728 | * The uri of image.
729 | * @param align
730 | * The alignment.
731 | *
732 | * - {@link Align#ALIGN_TOP }
733 | * - {@link Align#ALIGN_CENTER }
734 | * - {@link Align#ALIGN_BASELINE}
735 | * - {@link Align#ALIGN_BOTTOM }
736 | *
737 | * @return the single {@link SpanUtils} instance
738 | */
739 | public SpanUtils appendImage(@NonNull final Uri uri, @Align final int align) {
740 | apply(mTypeImage);
741 | this.imageUri = uri;
742 | this.alignImage = align;
743 | return this;
744 | }
745 |
746 | /**
747 | * Append one image.
748 | *
749 | * @param resourceId
750 | * The resource id of image.
751 | * @return the single {@link SpanUtils} instance
752 | */
753 | public SpanUtils appendImage(@DrawableRes final int resourceId) {
754 | return appendImage(resourceId, ALIGN_BOTTOM);
755 | }
756 |
757 | /**
758 | * Append one image.
759 | *
760 | * @param resourceId
761 | * The resource id of image.
762 | * @param align
763 | * The alignment.
764 | *
765 | * - {@link Align#ALIGN_TOP }
766 | * - {@link Align#ALIGN_CENTER }
767 | * - {@link Align#ALIGN_BASELINE}
768 | * - {@link Align#ALIGN_BOTTOM }
769 | *
770 | * @return the single {@link SpanUtils} instance
771 | */
772 | public SpanUtils appendImage(@DrawableRes final int resourceId, @Align final int align) {
773 | append(Character.toString((char) 0));// it's important for span start with image
774 | apply(mTypeImage);
775 | this.imageResourceId = resourceId;
776 | this.alignImage = align;
777 | return this;
778 | }
779 |
780 | /**
781 | * Append space.
782 | *
783 | * @param size
784 | * The size of space.
785 | * @return the single {@link SpanUtils} instance
786 | */
787 | public SpanUtils appendSpace(@IntRange(from = 0) final int size) {
788 | return appendSpace(size, Color.TRANSPARENT);
789 | }
790 |
791 | /**
792 | * Append space.
793 | *
794 | * @param size
795 | * The size of space.
796 | * @param color
797 | * The color of space.
798 | * @return the single {@link SpanUtils} instance
799 | */
800 | public SpanUtils appendSpace(@IntRange(from = 0) final int size, @ColorInt final int color) {
801 | apply(mTypeSpace);
802 | spaceSize = size;
803 | spaceColor = color;
804 | return this;
805 | }
806 |
807 | private void apply(final int type) {
808 | applyLast();
809 | mType = type;
810 | }
811 |
812 | /**
813 | * Create the span string.
814 | *
815 | * @return the span string
816 | */
817 | public SpannableStringBuilder create() {
818 | applyLast();
819 | return mBuilder;
820 | }
821 |
822 | private void applyLast() {
823 | if (mType == mTypeCharSequence) {
824 | updateCharCharSequence();
825 | } else if (mType == mTypeImage) {
826 | updateImage();
827 | } else if (mType == mTypeSpace) {
828 | updateSpace();
829 | }
830 | setDefault();
831 | }
832 |
833 | private void updateCharCharSequence() {
834 | if (mText.length() == 0) return;
835 | int start = mBuilder.length();
836 | mBuilder.append(mText);
837 | int end = mBuilder.length();
838 | if (foregroundColor != COLOR_DEFAULT) {
839 | mBuilder.setSpan(new ForegroundColorSpan(foregroundColor), start, end, flag);
840 | }
841 | if (backgroundColor != COLOR_DEFAULT) {
842 | mBuilder.setSpan(new BackgroundColorSpan(backgroundColor), start, end, flag);
843 | }
844 | if (first != -1) {
845 | mBuilder.setSpan(new LeadingMarginSpan.Standard(first, rest), start, end, flag);
846 | }
847 | if (quoteColor != COLOR_DEFAULT) {
848 | mBuilder.setSpan(
849 | new CustomQuoteSpan(quoteColor, stripeWidth, quoteGapWidth),
850 | start,
851 | end,
852 | flag
853 | );
854 | }
855 | if (bulletColor != COLOR_DEFAULT) {
856 | mBuilder.setSpan(
857 | new CustomBulletSpan(bulletColor, bulletRadius, bulletGapWidth),
858 | start,
859 | end,
860 | flag
861 | );
862 | }
863 | // if (imGapWidth != -1) {
864 | // if (imBitmap != null) {
865 | // mBuilder.setSpan(
866 | // new CustomIconMarginSpan(imBitmap, imGapWidth, imAlign),
867 | // start,
868 | // end,
869 | // flag
870 | // );
871 | // } else if (imDrawable != null) {
872 | // mBuilder.setSpan(
873 | // new CustomIconMarginSpan(imDrawable, imGapWidth, imAlign),
874 | // start,
875 | // end,
876 | // flag
877 | // );
878 | // } else if (imUri != null) {
879 | // mBuilder.setSpan(
880 | // new CustomIconMarginSpan(imUri, imGapWidth, imAlign),
881 | // start,
882 | // end,
883 | // flag
884 | // );
885 | // } else if (imResourceId != -1) {
886 | // mBuilder.setSpan(
887 | // new CustomIconMarginSpan(imResourceId, imGapWidth, imAlign),
888 | // start,
889 | // end,
890 | // flag
891 | // );
892 | // }
893 | // }
894 | if (fontSize != -1) {
895 | mBuilder.setSpan(new AbsoluteSizeSpan(fontSize, fontSizeIsDp), start, end, flag);
896 | }
897 | if (proportion != -1) {
898 | mBuilder.setSpan(new RelativeSizeSpan(proportion), start, end, flag);
899 | }
900 | if (xProportion != -1) {
901 | mBuilder.setSpan(new ScaleXSpan(xProportion), start, end, flag);
902 | }
903 | if (lineHeight != -1) {
904 | mBuilder.setSpan(new CustomLineHeightSpan(lineHeight, alignLine), start, end, flag);
905 | }
906 | if (isStrikethrough) {
907 | mBuilder.setSpan(new StrikethroughSpan(), start, end, flag);
908 | }
909 | if (isUnderline) {
910 | mBuilder.setSpan(new UnderlineSpan(), start, end, flag);
911 | }
912 | if (isSuperscript) {
913 | mBuilder.setSpan(new SuperscriptSpan(), start, end, flag);
914 | }
915 | if (isSubscript) {
916 | mBuilder.setSpan(new SubscriptSpan(), start, end, flag);
917 | }
918 | if (isBold) {
919 | mBuilder.setSpan(new StyleSpan(Typeface.BOLD), start, end, flag);
920 | }
921 | if (isItalic) {
922 | mBuilder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flag);
923 | }
924 | if (isBoldItalic) {
925 | mBuilder.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end, flag);
926 | }
927 | if (fontFamily != null) {
928 | mBuilder.setSpan(new TypefaceSpan(fontFamily), start, end, flag);
929 | }
930 | if (typeface != null) {
931 | mBuilder.setSpan(new CustomTypefaceSpan(typeface), start, end, flag);
932 | }
933 | if (alignment != null) {
934 | mBuilder.setSpan(new AlignmentSpan.Standard(alignment), start, end, flag);
935 | }
936 | if (clickSpan != null) {
937 | mBuilder.setSpan(clickSpan, start, end, flag);
938 | }
939 | if (url != null) {
940 | mBuilder.setSpan(new URLSpan(url), start, end, flag);
941 | }
942 | if (blurRadius != -1) {
943 | mBuilder.setSpan(
944 | new MaskFilterSpan(new BlurMaskFilter(blurRadius, style)),
945 | start,
946 | end,
947 | flag
948 | );
949 | }
950 | if (shader != null) {
951 | mBuilder.setSpan(new ShaderSpan(shader), start, end, flag);
952 | }
953 | if (shadowRadius != -1) {
954 | mBuilder.setSpan(
955 | new ShadowSpan(shadowRadius, shadowDx, shadowDy, shadowColor),
956 | start,
957 | end,
958 | flag
959 | );
960 | }
961 | if (spans != null) {
962 | for (Object span : spans) {
963 | mBuilder.setSpan(span, start, end, flag);
964 | }
965 | }
966 | }
967 |
968 | private void updateImage() {
969 | int start = mBuilder.length();
970 | mBuilder.append("
");
971 | int end = start + 5;
972 | if (imageBitmap != null) {
973 | mBuilder.setSpan(new CustomImageSpan(imageBitmap, alignImage), start, end, flag);
974 | } else if (imageDrawable != null) {
975 | mBuilder.setSpan(new CustomImageSpan(imageDrawable, alignImage), start, end, flag);
976 | } else if (imageUri != null) {
977 | mBuilder.setSpan(new CustomImageSpan(imageUri, alignImage), start, end, flag);
978 | } else if (imageResourceId != -1) {
979 | mBuilder.setSpan(new CustomImageSpan(imageResourceId, alignImage), start, end, flag);
980 | }
981 | }
982 |
983 | private void updateSpace() {
984 | int start = mBuilder.length();
985 | mBuilder.append("< >");
986 | int end = start + 3;
987 | mBuilder.setSpan(new SpaceSpan(spaceSize, spaceColor), start, end, flag);
988 | }
989 |
990 | class CustomLineHeightSpan extends CharacterStyle
991 | implements LineHeightSpan {
992 |
993 | private final int height;
994 |
995 | static final int ALIGN_CENTER = 2;
996 |
997 | static final int ALIGN_TOP = 3;
998 |
999 | final int mVerticalAlignment;
1000 |
1001 | CustomLineHeightSpan(int height, int verticalAlignment) {
1002 | this.height = height;
1003 | mVerticalAlignment = verticalAlignment;
1004 | }
1005 |
1006 | @Override
1007 | public void chooseHeight(final CharSequence text, final int start, final int end,
1008 | final int spanstartv, final int v, final Paint.FontMetricsInt fm) {
1009 | int need = height - (v + fm.descent - fm.ascent - spanstartv);
1010 | // if (need > 0) {
1011 | if (mVerticalAlignment == ALIGN_TOP) {
1012 | fm.descent += need;
1013 | } else if (mVerticalAlignment == ALIGN_CENTER) {
1014 | fm.descent += need / 2;
1015 | fm.ascent -= need / 2;
1016 | } else {
1017 | fm.ascent -= need;
1018 | }
1019 | // }
1020 | need = height - (v + fm.bottom - fm.top - spanstartv);
1021 | // if (need > 0) {
1022 | if (mVerticalAlignment == ALIGN_TOP) {
1023 | fm.top += need;
1024 | } else if (mVerticalAlignment == ALIGN_CENTER) {
1025 | fm.bottom += need / 2;
1026 | fm.top -= need / 2;
1027 | } else {
1028 | fm.top -= need;
1029 | }
1030 | // }
1031 | }
1032 |
1033 | @Override
1034 | public void updateDrawState(final TextPaint tp) {
1035 |
1036 | }
1037 | }
1038 |
1039 | class SpaceSpan extends ReplacementSpan {
1040 |
1041 | private final int width;
1042 | private final int color;
1043 |
1044 | private SpaceSpan(final int width) {
1045 | this(width, Color.TRANSPARENT);
1046 | }
1047 |
1048 | private SpaceSpan(final int width, final int color) {
1049 | super();
1050 | this.width = width;
1051 | this.color = color;
1052 | }
1053 |
1054 | @Override
1055 | public int getSize(@NonNull final Paint paint, final CharSequence text,
1056 | @IntRange(from = 0) final int start,
1057 | @IntRange(from = 0) final int end,
1058 | @Nullable final Paint.FontMetricsInt fm) {
1059 | return width;
1060 | }
1061 |
1062 | @Override
1063 | public void draw(@NonNull final Canvas canvas, final CharSequence text,
1064 | @IntRange(from = 0) final int start,
1065 | @IntRange(from = 0) final int end,
1066 | final float x, final int top, final int y, final int bottom,
1067 | @NonNull final Paint paint) {
1068 | Paint.Style style = paint.getStyle();
1069 | int color = paint.getColor();
1070 |
1071 | paint.setStyle(Paint.Style.FILL);
1072 | paint.setColor(this.color);
1073 |
1074 | canvas.drawRect(x, top, x + width, bottom, paint);
1075 |
1076 | paint.setStyle(style);
1077 | paint.setColor(color);
1078 | }
1079 | }
1080 |
1081 | class CustomQuoteSpan implements LeadingMarginSpan {
1082 |
1083 | private final int color;
1084 | private final int stripeWidth;
1085 | private final int gapWidth;
1086 |
1087 | private CustomQuoteSpan(final int color, final int stripeWidth, final int gapWidth) {
1088 | super();
1089 | this.color = color;
1090 | this.stripeWidth = stripeWidth;
1091 | this.gapWidth = gapWidth;
1092 | }
1093 |
1094 | public int getLeadingMargin(final boolean first) {
1095 | return stripeWidth + gapWidth;
1096 | }
1097 |
1098 | public void drawLeadingMargin(final Canvas c, final Paint p, final int x, final int dir,
1099 | final int top, final int baseline, final int bottom,
1100 | final CharSequence text, final int start, final int end,
1101 | final boolean first, final Layout layout) {
1102 | Paint.Style style = p.getStyle();
1103 | int color = p.getColor();
1104 |
1105 | p.setStyle(Paint.Style.FILL);
1106 | p.setColor(this.color);
1107 |
1108 | c.drawRect(x, top, x + dir * stripeWidth, bottom, p);
1109 |
1110 | p.setStyle(style);
1111 | p.setColor(color);
1112 | }
1113 | }
1114 |
1115 | class CustomBulletSpan implements LeadingMarginSpan {
1116 |
1117 | private final int color;
1118 | private final int radius;
1119 | private final int gapWidth;
1120 |
1121 | private Path sBulletPath = null;
1122 |
1123 | private CustomBulletSpan(final int color, final int radius, final int gapWidth) {
1124 | this.color = color;
1125 | this.radius = radius;
1126 | this.gapWidth = gapWidth;
1127 | }
1128 |
1129 | public int getLeadingMargin(final boolean first) {
1130 | return 2 * radius + gapWidth;
1131 | }
1132 |
1133 | public void drawLeadingMargin(final Canvas c, final Paint p, final int x, final int dir,
1134 | final int top, final int baseline, final int bottom,
1135 | final CharSequence text, final int start, final int end,
1136 | final boolean first, final Layout l) {
1137 | if (((Spanned) text).getSpanStart(this) == start) {
1138 | Paint.Style style = p.getStyle();
1139 | int oldColor = 0;
1140 | oldColor = p.getColor();
1141 | p.setColor(color);
1142 | p.setStyle(Paint.Style.FILL);
1143 | if (c.isHardwareAccelerated()) {
1144 | if (sBulletPath == null) {
1145 | sBulletPath = new Path();
1146 | // Bullet is slightly better to avoid aliasing artifacts on mdpi devices.
1147 | sBulletPath.addCircle(0.0f, 0.0f, radius, Path.Direction.CW);
1148 | }
1149 | c.save();
1150 | c.translate(x + dir * radius, (top + bottom) / 2.0f);
1151 | c.drawPath(sBulletPath, p);
1152 | c.restore();
1153 | } else {
1154 | c.drawCircle(x + dir * radius, (top + bottom) / 2.0f, radius, p);
1155 | }
1156 | p.setColor(oldColor);
1157 | p.setStyle(style);
1158 | }
1159 | }
1160 | }
1161 |
1162 | @SuppressLint("ParcelCreator")
1163 | class CustomTypefaceSpan extends TypefaceSpan {
1164 |
1165 | private final Typeface newType;
1166 |
1167 | private CustomTypefaceSpan(final Typeface type) {
1168 | super("");
1169 | newType = type;
1170 | }
1171 |
1172 | @Override
1173 | public void updateDrawState(final TextPaint textPaint) {
1174 | apply(textPaint, newType);
1175 | }
1176 |
1177 | @Override
1178 | public void updateMeasureState(final TextPaint paint) {
1179 | apply(paint, newType);
1180 | }
1181 |
1182 | private void apply(final Paint paint, final Typeface tf) {
1183 | int oldStyle;
1184 | Typeface old = paint.getTypeface();
1185 | if (old == null) {
1186 | oldStyle = 0;
1187 | } else {
1188 | oldStyle = old.getStyle();
1189 | }
1190 |
1191 | int fake = oldStyle & ~tf.getStyle();
1192 | if ((fake & Typeface.BOLD) != 0) {
1193 | paint.setFakeBoldText(true);
1194 | }
1195 |
1196 | if ((fake & Typeface.ITALIC) != 0) {
1197 | paint.setTextSkewX(-0.25f);
1198 | }
1199 |
1200 | paint.getShader();
1201 |
1202 | paint.setTypeface(tf);
1203 | }
1204 | }
1205 |
1206 | class CustomImageSpan extends CustomDynamicDrawableSpan {
1207 | private Drawable mDrawable;
1208 | private Uri mContentUri;
1209 | private int mResourceId;
1210 |
1211 | private CustomImageSpan(final Bitmap b, final int verticalAlignment) {
1212 | super(verticalAlignment);
1213 | mDrawable = new BitmapDrawable(mContext.getResources(), b);
1214 | mDrawable.setBounds(
1215 | 0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()
1216 | );
1217 | }
1218 |
1219 | private CustomImageSpan(final Drawable d, final int verticalAlignment) {
1220 | super(verticalAlignment);
1221 | mDrawable = d;
1222 | mDrawable.setBounds(
1223 | 0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()
1224 | );
1225 | }
1226 |
1227 | private CustomImageSpan(final Uri uri, final int verticalAlignment) {
1228 | super(verticalAlignment);
1229 | mContentUri = uri;
1230 | }
1231 |
1232 | private CustomImageSpan(@DrawableRes final int resourceId, final int verticalAlignment) {
1233 | super(verticalAlignment);
1234 | mResourceId = resourceId;
1235 | }
1236 |
1237 | @Override
1238 | public Drawable getDrawable() {
1239 | Drawable drawable = null;
1240 | if (mDrawable != null) {
1241 | drawable = mDrawable;
1242 | } else if (mContentUri != null) {
1243 | Bitmap bitmap;
1244 | try {
1245 | InputStream is =
1246 | mContext.getContentResolver().openInputStream(mContentUri);
1247 | bitmap = BitmapFactory.decodeStream(is);
1248 | drawable = new BitmapDrawable(mContext.getResources(), bitmap);
1249 | drawable.setBounds(
1250 | 0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()
1251 | );
1252 | if (is != null) {
1253 | is.close();
1254 | }
1255 | } catch (Exception e) {
1256 | Log.e("sms", "Failed to loaded content " + mContentUri, e);
1257 | }
1258 | } else {
1259 | try {
1260 | drawable = ContextCompat.getDrawable(mContext, mResourceId);
1261 | drawable.setBounds(
1262 | 0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()
1263 | );
1264 | } catch (Exception e) {
1265 | Log.e("sms", "Unable to find resource: " + mResourceId);
1266 | }
1267 | }
1268 | return drawable;
1269 | }
1270 | }
1271 |
1272 | abstract class CustomDynamicDrawableSpan extends ReplacementSpan {
1273 |
1274 | static final int ALIGN_BOTTOM = 0;
1275 |
1276 | static final int ALIGN_BASELINE = 1;
1277 |
1278 | static final int ALIGN_CENTER = 2;
1279 |
1280 | static final int ALIGN_TOP = 3;
1281 |
1282 | final int mVerticalAlignment;
1283 |
1284 | private CustomDynamicDrawableSpan() {
1285 | mVerticalAlignment = ALIGN_BOTTOM;
1286 | }
1287 |
1288 | private CustomDynamicDrawableSpan(final int verticalAlignment) {
1289 | mVerticalAlignment = verticalAlignment;
1290 | }
1291 |
1292 | public abstract Drawable getDrawable();
1293 |
1294 | @Override
1295 | public int getSize(@NonNull final Paint paint, final CharSequence text,
1296 | final int start, final int end, final Paint.FontMetricsInt fm) {
1297 | Drawable d = getCachedDrawable();
1298 | Rect rect = d.getBounds();
1299 | if (fm != null) {
1300 | // LogUtils.d("fm.top: " + fm.top,
1301 | // "fm.ascent: " + fm.ascent,
1302 | // "fm.descent: " + fm.descent,
1303 | // "fm.bottom: " + fm.bottom,
1304 | // "lineHeight: " + (fm.bottom - fm.top));
1305 | int lineHeight = fm.bottom - fm.top;
1306 | if (lineHeight < rect.height()) {
1307 | if (mVerticalAlignment == ALIGN_TOP) {
1308 | fm.top = fm.top;
1309 | fm.bottom = rect.height() + fm.top;
1310 | } else if (mVerticalAlignment == ALIGN_CENTER) {
1311 | fm.top = -rect.height() / 2 - lineHeight / 4;
1312 | fm.bottom = rect.height() / 2 - lineHeight / 4;
1313 | } else {
1314 | fm.top = -rect.height() + fm.bottom;
1315 | fm.bottom = fm.bottom;
1316 | }
1317 | fm.ascent = fm.top;
1318 | fm.descent = fm.bottom;
1319 | }
1320 | }
1321 | return rect.right;
1322 | }
1323 |
1324 | @Override
1325 | public void draw(@NonNull final Canvas canvas, final CharSequence text,
1326 | final int start, final int end, final float x,
1327 | final int top, final int y, final int bottom, @NonNull final Paint paint) {
1328 | Drawable d = getCachedDrawable();
1329 | Rect rect = d.getBounds();
1330 | canvas.save();
1331 | float transY;
1332 | int lineHeight = bottom - top;
1333 | // LogUtils.d("rectHeight: " + rect.height(),
1334 | // "lineHeight: " + (bottom - top));
1335 | if (rect.height() < lineHeight) {
1336 | if (mVerticalAlignment == ALIGN_TOP) {
1337 | transY = top;
1338 | } else if (mVerticalAlignment == ALIGN_CENTER) {
1339 | transY = (bottom + top - rect.height()) / 2;
1340 | } else if (mVerticalAlignment == ALIGN_BASELINE) {
1341 | transY = y - rect.height();
1342 | } else {
1343 | transY = bottom - rect.height();
1344 | }
1345 | canvas.translate(x, transY);
1346 | } else {
1347 | canvas.translate(x, top);
1348 | }
1349 | d.draw(canvas);
1350 | canvas.restore();
1351 | }
1352 |
1353 | private Drawable getCachedDrawable() {
1354 | WeakReference wr = mDrawableRef;
1355 | Drawable d = null;
1356 | if (wr != null) {
1357 | d = wr.get();
1358 | }
1359 | if (d == null) {
1360 | d = getDrawable();
1361 | mDrawableRef = new WeakReference<>(d);
1362 | }
1363 | return d;
1364 | }
1365 |
1366 | private WeakReference mDrawableRef;
1367 | }
1368 |
1369 | class ShaderSpan extends CharacterStyle implements UpdateAppearance {
1370 | private Shader mShader;
1371 |
1372 | private ShaderSpan(final Shader shader) {
1373 | this.mShader = shader;
1374 | }
1375 |
1376 | @Override
1377 | public void updateDrawState(final TextPaint tp) {
1378 | tp.setShader(mShader);
1379 | }
1380 | }
1381 |
1382 | class ShadowSpan extends CharacterStyle implements UpdateAppearance {
1383 | private float radius;
1384 | private float dx, dy;
1385 | private int shadowColor;
1386 |
1387 | private ShadowSpan(final float radius,
1388 | final float dx,
1389 | final float dy,
1390 | final int shadowColor) {
1391 | this.radius = radius;
1392 | this.dx = dx;
1393 | this.dy = dy;
1394 | this.shadowColor = shadowColor;
1395 | }
1396 |
1397 | @Override
1398 | public void updateDrawState(final TextPaint tp) {
1399 | tp.setShadowLayer(radius, dx, dy, shadowColor);
1400 | }
1401 | }
1402 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_btn.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_custom_update.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/app/src/main/res/drawable/bg_custom_update.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_custom_update_dialog.9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/app/src/main/res/drawable/bg_custom_update_dialog.9.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/app/src/main/res/drawable/ic_close.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/app/src/main/res/drawable/ic_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_update.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/app/src/main/res/drawable/ic_update.png
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_java_demo.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
21 |
22 |
28 |
29 |
35 |
36 |
42 |
43 |
44 |
50 |
51 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/check_md5_demo_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
22 |
23 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_update_dialog_custom.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
28 |
29 |
38 |
39 |
51 |
52 |
53 |
68 |
69 |
80 |
81 |
82 |
83 |
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 | #0076FF
7 | #333333
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Update
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.3.31'
5 |
6 | repositories {
7 | jcenter()
8 | google()
9 | maven { url 'https://jitpack.io' }
10 | }
11 | dependencies {
12 | classpath 'com.android.tools.build:gradle:3.4.1'
13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 | classpath 'com.novoda:bintray-release:0.9.1'
15 | }
16 | }
17 |
18 | allprojects {
19 |
20 | repositories {
21 | maven { url 'https://maven.aliyun.com/repository/public/' }
22 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' }
23 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' }
24 | maven { url 'http://maven.aliyun.com/nexus/content/repositories/gradle-plugin' }
25 | jcenter()
26 | google()
27 | maven { url 'https://jitpack.io' }
28 | }
29 |
30 | //中文注释
31 | tasks.withType(Javadoc) {
32 | options{ encoding "UTF-8"
33 | charSet 'UTF-8'
34 | links "http://docs.oracle.com/javase/7/docs/api"
35 | }
36 | }
37 | }
38 |
39 | task clean(type: Delete) {
40 | delete rootProject.buildDir
41 | }
42 |
43 |
44 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
19 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Jun 03 12:27:34 CST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/img/demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/img/demo.png
--------------------------------------------------------------------------------
/img/update_ui_change.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/img/update_ui_change.png
--------------------------------------------------------------------------------
/img/update_ui_custom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/img/update_ui_custom.png
--------------------------------------------------------------------------------
/img/update_ui_downloading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/img/update_ui_downloading.png
--------------------------------------------------------------------------------
/img/update_ui_fail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/img/update_ui_fail.png
--------------------------------------------------------------------------------
/img/update_ui_force.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/img/update_ui_force.png
--------------------------------------------------------------------------------
/img/update_ui_plentiful.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/img/update_ui_plentiful.png
--------------------------------------------------------------------------------
/img/update_ui_simple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/img/update_ui_simple.png
--------------------------------------------------------------------------------
/readme/README_1.5.2.md:
--------------------------------------------------------------------------------
1 | # updateapputils
2 |
3 | ### 一行代码,快速实现app在线下载更新 A simple library for Android update app
4 | ### 适配Android6.0、7.0、8.0
5 | 
6 |
7 | ## 集成
8 | compile引入
9 | ```
10 | dependencies {
11 | implementation 'com.teprinciple:updateapputils:1.5.2'
12 | }
13 | ```
14 |
15 | ## 使用
16 | 更新检测一般放在MainActivity或者启动页上,
17 | 在请求服务器版本检测接口获取到versionCode、versionName、最新apkPath后调用。
18 |
19 | #### 快速使用
20 | ```
21 | UpdateAppUtils.from(this)
22 | .serverVersionCode(2) //服务器versionCode
23 | .serverVersionName("2.0") //服务器versionName
24 | .apkPath(apkPath) //最新apk下载地址
25 | .update();
26 | ```
27 | #### Kotlin代码调用完全一样
28 | ```
29 | private fun update() {
30 | val apkPath:String = "http://issuecdn.baidupcs.com/issue/netdisk/apk/BaiduNetdisk_7.15.1.apk"
31 |
32 | UpdateAppUtils.from(this)
33 | .serverVersionCode(2)
34 | .serverVersionName("2.0")
35 | .apkPath(apkPath)
36 | .update()
37 | }
38 |
39 | ```
40 |
41 | #### 更多配置使用
42 | ```
43 | UpdateAppUtils.from(this)
44 | .checkBy(UpdateAppUtils.CHECK_BY_VERSION_NAME) //更新检测方式,默认为VersionCode
45 | .serverVersionCode(2)
46 | .serverVersionName("2.0")
47 | .apkPath(apkPath)
48 | .showNotification(false) //是否显示下载进度到通知栏,默认为true
49 | .updateInfo(info) //更新日志信息 String
50 | .downloadBy(UpdateAppUtils.DOWNLOAD_BY_BROWSER) //下载方式:app下载、手机浏览器下载。默认app下载
51 | .isForce(true) //是否强制更新,默认false 强制更新情况下用户不同意更新则不能使用app
52 | .update();
53 | ```
54 |
55 | #### 说明
56 | ```
57 | 1、UpdateAppUtils提供两种更新判断方式
58 |
59 | CHECK_BY_VERSION_CODE:通过versionCode判断,服务器上versionCode > 本地versionCode则执行更新
60 |
61 | CHECK_BY_VERSION_NAME:通过versionName判断,服务器上versionName 与 本地versionName不同则更新
62 |
63 | 2、UpdateAppUtils提供两种下载apk方式
64 |
65 | DOWNLOAD_BY_APP:通过App下载
66 |
67 | DOWNLOAD_BY_BROWSER:通过手机浏览器下载
68 |
69 | ```
70 |
71 | #### 关于适配Android6.0、7.0、8.0
72 |
73 | 库内部已经完全适配至8.0,你可以不用再对该库进行适配
74 |
75 | #### 文章地址:[《UpdateAppUtils一行代码实现app在线更新》](http://www.jianshu.com/p/9c91bb984c85)
76 |
77 | #### 更新日志
78 | 1.5.2
79 | 修复部分bug
80 |
1.5.1
81 | 库内部适配至Android8.0
82 |
1.4
83 | 使用[filedownloader](https://github.com/lingochamp/FileDownloader)替换DownloadManager,避免部分手机DownLoadManager无效,同时解决了重复下载的问题,且提高了下载速度
84 | 增加接口UpdateAppUtils.needFitAndroidN(false),避免不需要适配7.0,也要设置FileProvider
85 |
1.3.1
86 | 修复部分bug,在demo中加入kotlin调用代码
87 |
1.3
88 | 增加接口方法 showNotification(false)//是否显示下载进度到通知栏;
updateInfo(info)//更新日志信息;下载前WiFi判断。
89 |
1.2
90 | 适配Android7.0,并在demo中加入适配6.0和7.0的代码
91 |
1.1
92 | 适配更多SdkVersion
--------------------------------------------------------------------------------
/readme/version.md:
--------------------------------------------------------------------------------
1 | ### 更新日志
2 | #### 2.3.0
3 | * 修复部分手机context空指针异常
4 | #### 2.2.1
5 | * 优化代码
6 | * 修复部分bug
7 | #### 2.2.0
8 | * 适配Android 10
9 | * 修复部分bug
10 | #### 2.1.0
11 | * 增加'暂不更新'按钮点击监听 setCancelBtnClickListener()
12 | * 增加'立即更新'按钮点击监听 setUpdateBtnClickListener()
13 | * 修复部分bug
14 | #### 2.0.4
15 | * 修复阿里云,码云平台上的apk FileDownloader下载失败
16 | * 增加UpdateConfig alwaysShowDownLoadDialog字段,让非强更也能显示下载进度弹窗
17 | #### 2.0.3
18 | * 更新弹窗内容支持SpannableString
19 | #### 2.0.2
20 | * 9.0Http适配
21 | #### 2.0.1
22 | * 自定义FileProvide,防止provider冲突
23 | #### 2.0.0
24 | * Kotlin重构
25 | * 支持AndroidX
26 | * 安装包签名文件md5校验
27 | * 通知栏自定义图标
28 | * 支持自定义UI
29 | * 适配中英文
30 | * 增加下载回调等api
31 | * 修复部分bug
32 | #### 1.5.2
33 | * 修复部分bug
34 | #### 1.5.1
35 | * 库内部适配至Android8.0
36 | #### 1.4
37 | * 使用[filedownloader](https://github.com/lingochamp/FileDownloader)替换DownloadManager,避免部分手机DownLoadManager无效,同时解决了重复下载的问题,且提高了下载速度
38 | * 增加接口UpdateAppUtils.needFitAndroidN(false),避免不需要适配7.0,也要设置FileProvider
39 | #### 1.3.1
40 | * 修复部分bug,在demo中加入kotlin调用代码
41 | #### 1.3
42 | * 增加接口方法 showNotification(false) //是否显示下载进度到通知栏;
43 | * updateInfo(info) //更新日志信息;
44 | * 下载前WiFi判断。
45 | #### 1.2
46 | * 适配Android7.0,并在demo中加入适配6.0和7.0的代码
47 | #### 1.1
48 | * 适配更多SdkVersion
--------------------------------------------------------------------------------
/readme/自定义UI.md:
--------------------------------------------------------------------------------
1 | ## 完全自定义UI
2 |
3 | ### 1、创建你的layout(必须)
4 | 你可以创建任意你想要的UI布局([参考 view_update_dialog_custom.xml](https://github.com/teprinciple/UpdateAppUtils/blob/master/app/src/main/res/layout/view_update_dialog_custom.xml))
5 | ,但是控件id需要保持如下:
6 |
7 | | id | 说明 | 控件类型 | 是否必须 |
8 | |:--------------------- |:-------------------|:----------------- |:------ |
9 | | btn_update_sure | 立即更新按钮id| 任意View |true |
10 | | btn_update_cancel | 暂不更新按钮id| 任意View |true |
11 | | tv_update_title | 更新弹窗标题| TextView |false |
12 | | tv_update_content | 更新内容| TextView |false |
13 |
14 | btn_update_sure和btn_update_cancel是必须提供的,否则更新无法继续;
15 |
16 | tv_update_title,tv_update_content提供,UpdateAppUtils内部会自动
17 | 设置值,如果你不想这样,也可以自行命名,稍后通过OnInitUiListener接口进行相关文案设置;
18 |
19 | ### 2、注入到UpdateAppUtils(必须)
20 |
21 | 通过设置uiConfig,将自定义布局注入到UpdateAppUtils;注意uiType必须为UiType.CUSTOM
22 |
23 | ```
24 | UpdateAppUtils
25 | .getInstance()
26 | // ...
27 | .uiConfig(UiConfig(uiType = UiType.CUSTOM, customLayoutId = R.layout.view_update_dialog_custom))
28 | .update()
29 | ```
30 |
31 | ### 3、实现OnInitUiListener接口(非必须)
32 |
33 | UpdateAppUtils 中只对上表中的4个控件进行了相关内容的填充,如果你自定义的布局中有其他控件需要进行内容填充
34 | 需要实现OnInitUiListener接口来进行操作:
35 | ```
36 | UpdateAppUtils
37 | .getInstance()
38 | // ...
39 | .setOnInitUiListener(object : OnInitUiListener {
40 | override fun onInitUpdateUi(view: View?, updateConfig: UpdateConfig, uiConfig: UiConfig) {
41 | view?.findViewById(R.id.tv_update_title)?.text = "版本更新啦"
42 | view?.findViewById(R.id.tv_version_name)?.text = "V2.0.0"
43 | // do more...
44 | }
45 | })
46 | ```
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':updateapputils'
2 |
--------------------------------------------------------------------------------
/update.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/update.gif
--------------------------------------------------------------------------------
/update.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/update.jks
--------------------------------------------------------------------------------
/updateapputils/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/updateapputils/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android-extensions'
3 | apply plugin: 'kotlin-android'
4 | apply plugin: 'com.novoda.bintray-release'
5 |
6 | android {
7 | compileSdkVersion 29
8 | defaultConfig {
9 | minSdkVersion 19
10 | targetSdkVersion 29
11 | }
12 | buildTypes {
13 | release {
14 | minifyEnabled false
15 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
16 | }
17 | }
18 |
19 | // 忽略错误信息
20 | lintOptions {
21 | abortOnError false
22 | }
23 |
24 | androidExtensions {
25 | experimental = true
26 | }
27 | }
28 |
29 | dependencies {
30 | implementation fileTree(include: ['*.jar'], dir: 'libs')
31 | implementation 'com.android.support:appcompat-v7:28.0.0'
32 | implementation 'com.android.support.constraint:constraint-layout:1.1.3'
33 | implementation 'com.liulishuo.filedownloader:library:1.7.7'
34 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
35 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
36 | }
37 |
38 | repositories {
39 | mavenCentral()
40 | }
41 |
42 | publish {
43 | userOrg = 'teprinciple'
44 | groupId = 'com.teprinciple'
45 | artifactId = 'updateapputils'
46 | publishVersion = '2.3.0'
47 | desc = 'A Simple library for Android update app'
48 | website = 'https://github.com/teprinciple/UpdateAppUtils'
49 | }
--------------------------------------------------------------------------------
/updateapputils/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/teprinciple/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/updateapputils/src/androidTest/java/teprinciple/library/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package teprinciple.library;
2 |
3 | import android.content.Context;
4 | import android.support.test.InstrumentationRegistry;
5 | import android.support.test.runner.AndroidJUnit4;
6 |
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 |
10 | import static org.junit.Assert.*;
11 |
12 | /**
13 | * Instrumentation test, which will execute on an Android device.
14 | *
15 | * @see Testing documentation
16 | */
17 | @RunWith(AndroidJUnit4.class)
18 | public class ExampleInstrumentedTest {
19 | @Test
20 | public void useAppContext() throws Exception {
21 | // Context of the app under test.
22 | Context appContext = InstrumentationRegistry.getTargetContext();
23 |
24 | assertEquals("teprinciple.library.test", appContext.getPackageName());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/updateapputils/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
24 |
25 |
26 |
27 |
32 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/updateapputils/src/main/java/constant/DownLoadBy.kt:
--------------------------------------------------------------------------------
1 | package constant
2 |
3 | /**
4 | * desc: 下载方式
5 | * time: 2019/6/18
6 | * @author yk
7 | */
8 | object DownLoadBy {
9 | /**
10 | * app下载
11 | */
12 | const val APP = 0x101
13 |
14 | /**
15 | * 浏览器下载
16 | */
17 | const val BROWSER = 0x102
18 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/constant/UiType.kt:
--------------------------------------------------------------------------------
1 | package constant
2 |
3 | /**
4 | * desc: UI 类型
5 | * time: 2019/6/27
6 | * @author yk
7 | */
8 | object UiType {
9 |
10 | /**
11 | * 简洁版
12 | */
13 | const val SIMPLE = "SIMPLE"
14 |
15 | /**
16 | * 丰富版
17 | */
18 | const val PLENTIFUL = "PLENTIFUL"
19 |
20 | /**
21 | * 全自定义
22 | */
23 | const val CUSTOM = "CUSTOM"
24 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/extension/BooleanKtx.kt:
--------------------------------------------------------------------------------
1 | package extension
2 |
3 | import kotlin.contracts.ExperimentalContracts
4 | import kotlin.contracts.InvocationKind
5 | import kotlin.contracts.contract
6 |
7 | @UseExperimental(ExperimentalContracts::class)
8 | inline fun Boolean?.yes(block: () -> Unit): Boolean? {
9 | contract {
10 | callsInPlace(block, InvocationKind.AT_MOST_ONCE)
11 | }
12 | if (this == true) block()
13 | return this
14 | }
15 |
16 | @UseExperimental(ExperimentalContracts::class)
17 | inline fun Boolean?.no(block: () -> Unit): Boolean? {
18 | contract {
19 | callsInPlace(block, InvocationKind.AT_MOST_ONCE)
20 | }
21 | if (this != true) block()
22 | return this
23 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/extension/ContextKtx.kt:
--------------------------------------------------------------------------------
1 | package extension
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.ConnectivityManager
6 | import android.net.Uri
7 | import android.os.Build
8 | import android.support.v4.content.FileProvider
9 | import java.io.File
10 |
11 | /**
12 | * desc: context 相关扩展
13 | * author: teprinciple on 2020/3/27.
14 | */
15 |
16 |
17 | /**
18 | * appName
19 | */
20 | val Context.appName
21 | get() = packageManager.getPackageInfo(packageName, 0)?.applicationInfo?.loadLabel(packageManager).toString()
22 |
23 | /**
24 | * 检测wifi是否连接
25 | */
26 | fun Context.isWifiConnected(): Boolean {
27 | val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
28 | cm ?: return false
29 | val networkInfo = cm.activeNetworkInfo
30 | return networkInfo != null && networkInfo.type == ConnectivityManager.TYPE_WIFI
31 | }
32 |
33 |
34 | /**
35 | * 跳转安装
36 | */
37 | fun Context.installApk(apkPath: String?) {
38 |
39 | if (apkPath.isNullOrEmpty())return
40 |
41 | val intent = Intent(Intent.ACTION_VIEW)
42 | val apkFile = File(apkPath)
43 |
44 | // android 7.0 fileprovider 适配
45 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
46 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
47 | val contentUri = FileProvider.getUriForFile(this, this.packageName + ".fileprovider", apkFile)
48 | intent.setDataAndType(contentUri, "application/vnd.android.package-archive")
49 | } else {
50 | intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive")
51 | }
52 |
53 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
54 | this.startActivity(intent)
55 | }
56 |
57 |
58 |
--------------------------------------------------------------------------------
/updateapputils/src/main/java/extension/CoreKtx.kt:
--------------------------------------------------------------------------------
1 | package extension
2 |
3 | import android.app.ActivityManager
4 | import android.content.Context
5 | import android.os.Build
6 | import android.support.v4.content.ContextCompat
7 | import android.util.Log
8 | import android.view.View
9 | import update.UpdateAppUtils
10 | import util.GlobalContextProvider
11 | import kotlin.system.exitProcess
12 |
13 | /**
14 | * desc: 扩展
15 | * author: teprinc
16 | * iple on 2020/3/27.
17 | */
18 |
19 | /**
20 | * 全局context
21 | */
22 | fun globalContext() = GlobalContextProvider.mContext
23 |
24 |
25 | /**
26 | * 打印日志
27 | */
28 | fun log(content: String?) = UpdateAppUtils.updateInfo.config.isDebug.yes {
29 | Log.e("[UpdateAppUtils]", content ?: "")
30 | }
31 |
32 | /**
33 | * 获取color
34 | */
35 | fun color(color: Int) = if (globalContext() == null) 0 else ContextCompat.getColor(globalContext()!!, color)
36 |
37 | /**
38 | * 获取 String
39 | */
40 | fun string(string: Int) = globalContext()?.getString(string) ?: ""
41 |
42 | /**
43 | * view 显示隐藏
44 | */
45 | fun View.visibleOrGone(show: Boolean) {
46 | if (show) {
47 | this.visibility = View.VISIBLE
48 | } else {
49 | this.visibility = View.GONE
50 | }
51 | }
52 |
53 | /**
54 | * 退出app
55 | */
56 | fun exitApp() {
57 | val manager = globalContext()!!.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
58 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
59 | manager.appTasks.forEach { it.finishAndRemoveTask() }
60 | } else {
61 | exitProcess(0)
62 | }
63 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/extension/StringKtx.kt:
--------------------------------------------------------------------------------
1 | package extension
2 |
3 | import java.io.File
4 |
5 | /**
6 | * desc: string 相关扩展
7 | * author: teprinciple on 2020/3/27.
8 | */
9 |
10 | /**
11 | * 根据文件路径删除文件
12 | */
13 | fun String?.deleteFile() {
14 | kotlin.runCatching {
15 | val file = File(this ?: "")
16 | (file.isFile).yes {
17 | file.delete()
18 | log("删除成功")
19 | }
20 | }.onFailure {
21 | log(it.message)
22 | }
23 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/listener/Md5CheckResultListener.kt:
--------------------------------------------------------------------------------
1 | package listener
2 |
3 | /**
4 | * desc: Md5校验结果回调
5 | * time: 2019/6/21
6 | * @author teprinciple
7 | */
8 | interface Md5CheckResultListener {
9 | fun onResult(result: Boolean)
10 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/listener/OnBtnClickListener.kt:
--------------------------------------------------------------------------------
1 | package listener
2 |
3 | /**
4 | * desc: 按钮点击监听
5 | * time: 2019/9/16
6 | * @author teprinciple
7 | */
8 | interface OnBtnClickListener {
9 |
10 | /**
11 | * 按钮点击
12 | * @return 是否消费事件
13 | */
14 | fun onClick(): Boolean
15 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/listener/OnInitUiListener.kt:
--------------------------------------------------------------------------------
1 | package listener
2 |
3 | import android.view.View
4 | import model.UiConfig
5 | import model.UpdateConfig
6 |
7 | /**
8 | * desc: 初始化UI 回调 用于进一步自定义UI
9 | * time: 2019/6/28
10 | * @author teprinciple
11 | */
12 | interface OnInitUiListener {
13 |
14 | /**
15 | * 初始化更新弹窗回调
16 | * @param view 弹窗view
17 | * @param updateConfig 当前更新配置
18 | * @param uiConfig 当前ui配置
19 | */
20 | fun onInitUpdateUi(view: View?, updateConfig: UpdateConfig, uiConfig: UiConfig)
21 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/listener/UpdateDownloadListener.kt:
--------------------------------------------------------------------------------
1 | package listener
2 |
3 | /**
4 | * desc: 下载监听
5 | * time: 2019/6/19
6 | * @author teprinciple
7 | */
8 | interface UpdateDownloadListener {
9 |
10 | /**
11 | * 开始下载
12 | */
13 | fun onStart()
14 |
15 | /**
16 | * 下载中
17 | * @param progress 进度 0 - 100
18 | */
19 | fun onDownload(progress: Int)
20 |
21 | /**
22 | * 下载完成
23 | */
24 | fun onFinish()
25 |
26 | /**
27 | * 下载错误
28 | */
29 | fun onError(e: Throwable)
30 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/model/UiConfig.kt:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import com.teprinciple.updateapputils.R
4 | import constant.UiType
5 | import extension.string
6 |
7 | /**
8 | * desc: UiConfig UI 配置
9 | * time: 2019/6/27
10 | * @author teprinciple
11 | */
12 | data class UiConfig(
13 | // ui类型,默认简洁版
14 | var uiType: String = UiType.SIMPLE,
15 | // 自定义UI 布局id
16 | var customLayoutId: Int? = null,
17 | // 更新弹窗中的logo
18 | var updateLogoImgRes: Int? = null,
19 | // 标题相关设置
20 | var titleTextSize: Float? = null,
21 | var titleTextColor: Int? = null,
22 | // 更新内容相关设置
23 | var contentTextSize: Float? = null,
24 | var contentTextColor: Int? = null,
25 | // 更新按钮相关设置
26 | var updateBtnBgColor: Int? = null,
27 | var updateBtnBgRes: Int? = null,
28 | var updateBtnTextColor: Int? = null,
29 | var updateBtnTextSize: Float? = null,
30 | var updateBtnText: CharSequence = string(R.string.update_now),
31 | // 取消按钮相关设置
32 | var cancelBtnBgColor: Int? = null,
33 | var cancelBtnBgRes: Int? = null,
34 | var cancelBtnTextColor: Int? = null,
35 | var cancelBtnTextSize: Float? = null,
36 | var cancelBtnText: CharSequence = string(R.string.update_cancel),
37 |
38 | // 开始下载时的Toast提示文字
39 | var downloadingToastText: CharSequence = string(R.string.toast_download_apk),
40 | // 下载中 下载按钮以及通知栏标题前缀,进度自动拼接在后面
41 | var downloadingBtnText: CharSequence = string(R.string.downloading),
42 | // 下载出错时,下载按钮及通知栏标题
43 | var downloadFailText: CharSequence = string(R.string.download_fail)
44 | )
--------------------------------------------------------------------------------
/updateapputils/src/main/java/model/UpdateConfig.kt:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import constant.DownLoadBy
4 |
5 | data class UpdateConfig(
6 | var isDebug: Boolean = true, // 是否是调试模式,调试模式会输出日志
7 |
8 | var alwaysShow: Boolean = true, // 非强制更新时,是否每次都显示弹窗,用VersionName来判断
9 | var thisTimeShow: Boolean = false, // 非强制更新时,指定本次显示弹窗
10 | var alwaysShowDownLoadDialog: Boolean = false, // 非强制更新时,也显示下载进度dialog
11 | var force: Boolean = false, // 是否强制更新
12 | var apkSavePath: String = "", // apk下载存放位置
13 | var apkSaveName: String = "", // apk 保存名(默认是app的名字)
14 | var downloadBy: Int = DownLoadBy.APP, // 下载方式:默认app下载
15 | //var downloadDirect: Boolean = false, // 不需要弹窗,直接开始下载安装
16 | var checkWifi: Boolean = false, // 是否检查是否wifi
17 | var isShowNotification: Boolean = true, // 是否在通知栏显示
18 | var notifyImgRes: Int = 0, // 通知栏图标
19 | var needCheckMd5: Boolean = false, // 是否需要进行md5校验,仅app下载方式有效
20 | var showDownloadingToast: Boolean = true, // 是否需要显示 【更新下载中】文案
21 | var serverVersionName: String = "", // 服务器上版本名
22 | var serverVersionCode: Int = 0 // 服务器上版本号
23 | )
--------------------------------------------------------------------------------
/updateapputils/src/main/java/model/UpdateInfo.kt:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import com.teprinciple.updateapputils.R
4 | import extension.string
5 |
6 | /**
7 | * desc: UpdateInfo
8 | * time: 2019/6/18
9 | * @author teprinciple
10 | */
11 | internal data class UpdateInfo(
12 | // 更新标题
13 | var updateTitle: CharSequence = string(R.string.update_title),
14 | // 更新内容
15 | var updateContent: CharSequence = string(R.string.update_content),
16 | // apk 下载地址
17 | var apkUrl: String = "",
18 | // 更新配置
19 | var config: UpdateConfig = UpdateConfig(),
20 | // ui配置
21 | var uiConfig: UiConfig = UiConfig()
22 | )
--------------------------------------------------------------------------------
/updateapputils/src/main/java/ui/UpdateAppActivity.kt:
--------------------------------------------------------------------------------
1 | package ui
2 |
3 | import android.Manifest
4 | import android.annotation.SuppressLint
5 | import android.content.Intent
6 | import android.content.pm.PackageManager
7 | import android.net.Uri
8 | import android.os.Build
9 | import android.os.Bundle
10 | import android.provider.Settings
11 | import android.support.v4.app.ActivityCompat
12 | import android.support.v4.content.ContextCompat
13 | import android.support.v7.app.AppCompatActivity
14 | import android.view.MotionEvent
15 | import android.view.View
16 | import android.widget.ImageView
17 | import android.widget.TextView
18 | import android.widget.Toast
19 | import com.teprinciple.updateapputils.R
20 | import constant.DownLoadBy
21 | import constant.UiType
22 | import extension.*
23 | import update.DownloadAppUtils
24 | import update.UpdateAppService
25 | import update.UpdateAppUtils
26 | import util.AlertDialogUtil
27 | import util.GlobalContextProvider
28 | import util.SPUtil
29 |
30 | /**
31 | * desc: 更新弹窗
32 | * author: teprinciple on 2019/06/3.
33 | */
34 | internal class UpdateAppActivity : AppCompatActivity() {
35 |
36 | private var tvTitle: TextView? = null
37 | private var tvContent: TextView? = null
38 | private var sureBtn: View? = null
39 | private var cancelBtn: View? = null
40 | private var ivLogo: ImageView? = null
41 |
42 | /**
43 | * 更新信息
44 | */
45 | private val updateInfo by lazy { UpdateAppUtils.updateInfo }
46 |
47 | /**
48 | * 更新配置
49 | */
50 | private val updateConfig by lazy { updateInfo.config }
51 |
52 | /**
53 | * ui 配置
54 | */
55 | private val uiConfig by lazy { updateInfo.uiConfig }
56 |
57 | override fun onCreate(savedInstanceState: Bundle?) {
58 | super.onCreate(savedInstanceState)
59 |
60 | if (GlobalContextProvider.mContext == null){
61 | GlobalContextProvider.mContext = this.applicationContext
62 | }
63 |
64 | setContentView(
65 | when (uiConfig.uiType) {
66 | UiType.SIMPLE -> R.layout.view_update_dialog_simple
67 | UiType.PLENTIFUL -> R.layout.view_update_dialog_plentiful
68 | UiType.CUSTOM -> uiConfig.customLayoutId ?: R.layout.view_update_dialog_simple
69 | else -> R.layout.view_update_dialog_simple
70 | }
71 | )
72 | initView()
73 | initUi()
74 |
75 | // 初始化UI回调,用于进一步自定义UI
76 | UpdateAppUtils.onInitUiListener?.onInitUpdateUi(
77 | window.decorView.findViewById(android.R.id.content),
78 | updateConfig,
79 | uiConfig)
80 |
81 | // 每次弹窗后,下载前均把本地之前缓存的apk删除,避免缓存老版本apk或者问题apk,并不重新下载新的apk
82 | SPUtil.getString(DownloadAppUtils.KEY_OF_SP_APK_PATH, "").deleteFile()
83 | }
84 |
85 | @SuppressLint("ClickableViewAccessibility")
86 | private fun initView() {
87 |
88 | tvTitle = findViewById(R.id.tv_update_title)
89 | tvContent = findViewById(R.id.tv_update_content)
90 | cancelBtn = findViewById(R.id.btn_update_cancel)
91 | sureBtn = findViewById(R.id.btn_update_sure)
92 | ivLogo = findViewById(R.id.iv_update_logo)
93 |
94 | // 更新标题
95 | tvTitle?.text = updateInfo.updateTitle
96 |
97 | // 更新内容
98 | tvContent?.text = updateInfo.updateContent
99 |
100 | // 取消
101 | cancelBtn?.setOnClickListener {
102 | updateConfig.force.yes {
103 | exitApp()
104 | }.no {
105 | finish()
106 | }
107 | }
108 |
109 | // 确定
110 | sureBtn?.setOnClickListener {
111 |
112 | DownloadAppUtils.isDownloading.no {
113 | if (sureBtn is TextView) {
114 | (sureBtn as? TextView)?.text = uiConfig.updateBtnText
115 | }
116 | preDownLoad()
117 | }
118 | }
119 |
120 | // 显示或隐藏取消按钮, 强更时默认不显示取消按钮
121 | hideShowCancelBtn(!updateConfig.force)
122 |
123 | // 外部额外设置 取消 按钮点击事件
124 | cancelBtn?.setOnTouchListener { v, event ->
125 | when (event.action) {
126 | MotionEvent.ACTION_UP -> {
127 | UpdateAppUtils.onCancelBtnClickListener?.onClick() ?: false
128 | }
129 | else -> false
130 | }
131 | }
132 |
133 | // 外部额外设置 立即更新 按钮点击事件
134 | sureBtn?.setOnTouchListener { v, event ->
135 | when (event.action) {
136 | MotionEvent.ACTION_UP -> {
137 | UpdateAppUtils.onUpdateBtnClickListener?.onClick() ?: false
138 | }
139 | else -> false
140 | }
141 | }
142 | }
143 |
144 | /**
145 | * 取消按钮处理
146 | */
147 | private fun hideShowCancelBtn(show: Boolean) {
148 | // 强制更新 不显示取消按钮
149 | cancelBtn?.visibleOrGone(show)
150 | // 取消按钮与确定按钮中的间隔线
151 | findViewById(R.id.view_line)?.visibleOrGone(show)
152 | }
153 |
154 | /**
155 | * 初始化UI
156 | */
157 | private fun initUi() {
158 |
159 | uiConfig.apply {
160 | // 设置更新logo
161 | updateLogoImgRes?.let { ivLogo?.setImageResource(it) }
162 | // 设置标题字体颜色、大小
163 | titleTextColor?.let { tvTitle?.setTextColor(it) }
164 | titleTextSize?.let { tvTitle?.setTextSize(it) }
165 | // 设置标题字体颜色、大小
166 | contentTextColor?.let { tvContent?.setTextColor(it) }
167 | contentTextSize?.let { tvContent?.setTextSize(it) }
168 | // 更新按钮相关设置
169 | updateBtnBgColor?.let { sureBtn?.setBackgroundColor(it) }
170 | updateBtnBgRes?.let { sureBtn?.setBackgroundResource(it) }
171 | if (sureBtn is TextView) {
172 | updateBtnTextColor?.let { (sureBtn as? TextView)?.setTextColor(it) }
173 | updateBtnTextSize?.let { (sureBtn as? TextView)?.setTextSize(it) }
174 | (sureBtn as? TextView)?.text = updateBtnText
175 | }
176 |
177 | // 取消按钮相关设置
178 | cancelBtnBgColor?.let { cancelBtn?.setBackgroundColor(it) }
179 | cancelBtnBgRes?.let { cancelBtn?.setBackgroundResource(it) }
180 | if (cancelBtn is TextView) {
181 | cancelBtnTextColor?.let { (cancelBtn as? TextView)?.setTextColor(it) }
182 | cancelBtnTextSize?.let { (cancelBtn as? TextView)?.setTextSize(it) }
183 | (cancelBtn as? TextView)?.text = cancelBtnText
184 | }
185 | }
186 | }
187 |
188 | override fun onBackPressed() {
189 | // do noting 禁用返回键
190 | }
191 |
192 | /**
193 | * 预备下载 进行 6.0权限检查
194 | */
195 | private fun preDownLoad() {
196 | // 6.0 以下不用动态权限申请
197 | (Build.VERSION.SDK_INT < Build.VERSION_CODES.M ).yes {
198 | download()
199 | }.no {
200 | val writePermission = ContextCompat.checkSelfPermission(this, permission)
201 | (writePermission == PackageManager.PERMISSION_GRANTED).yes {
202 | download()
203 | }.no {
204 | // 申请权限
205 | ActivityCompat.requestPermissions(this, arrayOf(permission), PERMISSION_CODE)
206 | }
207 | }
208 | }
209 |
210 | /**
211 | * 下载判断
212 | */
213 | private fun download() {
214 | // 动态注册广播,8.0 静态注册收不到
215 | // 开启服务注册,避免直接在Activity中注册广播生命周期随Activity终止而终止
216 | startService(Intent(this, UpdateAppService::class.java))
217 |
218 | when (updateConfig.downloadBy) {
219 | // App下载
220 | DownLoadBy.APP -> {
221 | (updateConfig.checkWifi && !isWifiConnected()).yes {
222 | // 需要进行WiFi判断
223 | AlertDialogUtil.show(this, getString(R.string.check_wifi_notice), onSureClick = {
224 | realDownload()
225 | })
226 | }.no {
227 | // 不需要wifi判断,直接下载
228 | realDownload()
229 | }
230 | }
231 |
232 | // 浏览器下载
233 | DownLoadBy.BROWSER -> {
234 | DownloadAppUtils.downloadForWebView(updateInfo.apkUrl)
235 | }
236 | }
237 | }
238 |
239 | /**
240 | * 实际下载
241 | */
242 | @SuppressLint("SetTextI18n")
243 | private fun realDownload() {
244 |
245 | if ((updateConfig.force || updateConfig.alwaysShowDownLoadDialog) && sureBtn is TextView) {
246 | DownloadAppUtils.onError = {
247 | (sureBtn as? TextView)?.text = uiConfig.downloadFailText
248 | (updateConfig.alwaysShowDownLoadDialog).yes {
249 | hideShowCancelBtn(true)
250 | }
251 | }
252 |
253 | DownloadAppUtils.onReDownload = {
254 | (sureBtn as? TextView)?.text = uiConfig.updateBtnText
255 | }
256 |
257 | DownloadAppUtils.onProgress = {
258 | (it == 100).yes {
259 | (sureBtn as? TextView)?.text = getString(R.string.install)
260 | (updateConfig.alwaysShowDownLoadDialog).yes {
261 | hideShowCancelBtn(true)
262 | }
263 | }.no {
264 | (sureBtn as? TextView)?.text = "${uiConfig.downloadingBtnText}$it%"
265 | (updateConfig.alwaysShowDownLoadDialog).yes {
266 | hideShowCancelBtn(false)
267 | }
268 | }
269 | }
270 | }
271 |
272 | DownloadAppUtils.download()
273 |
274 | (updateConfig.showDownloadingToast).yes {
275 | Toast.makeText(this, uiConfig.downloadingToastText, Toast.LENGTH_SHORT).show()
276 | }
277 |
278 | // 非强制安装且alwaysShowDownLoadDialog为false时,开始下载后取消弹窗
279 | (!updateConfig.force && !updateConfig.alwaysShowDownLoadDialog).yes {
280 | finish()
281 | }
282 | }
283 |
284 | /**
285 | * 权限请求结果
286 | */
287 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
288 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
289 |
290 | when (requestCode) {
291 | PERMISSION_CODE -> (grantResults.getOrNull(0) == PackageManager.PERMISSION_GRANTED).yes {
292 | download()
293 | }.no {
294 | ActivityCompat.shouldShowRequestPermissionRationale(this, permission).no {
295 | // 显示无权限弹窗
296 | AlertDialogUtil.show(this, getString(R.string.no_storage_permission), onSureClick = {
297 | val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
298 | intent.data = Uri.parse("package:$packageName") // 根据包名打开对应的设置界面
299 | startActivity(intent)
300 | })
301 | }
302 | }
303 | }
304 | }
305 |
306 | override fun finish() {
307 | super.finish()
308 | overridePendingTransition(0, 0)
309 | }
310 |
311 | companion object {
312 |
313 | fun launch() = globalContext()?.let {
314 | val intent = Intent(it, UpdateAppActivity::class.java)
315 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
316 | it.startActivity(intent)
317 | }
318 |
319 | private const val permission = Manifest.permission.WRITE_EXTERNAL_STORAGE
320 |
321 | private const val PERMISSION_CODE = 1001
322 | }
323 | }
324 |
--------------------------------------------------------------------------------
/updateapputils/src/main/java/update/DownloadAppUtils.kt:
--------------------------------------------------------------------------------
1 | package update
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Build
7 | import android.os.Environment
8 | import com.liulishuo.filedownloader.BaseDownloadTask
9 | import com.liulishuo.filedownloader.FileDownloadLargeFileListener
10 | import com.liulishuo.filedownloader.FileDownloader
11 | import extension.*
12 | import util.FileDownloadUtil
13 | import util.SPUtil
14 | import util.SignMd5Util
15 | import java.io.File
16 |
17 | /**
18 | * Created by Teprinciple on 2016/12/13.
19 | */
20 | internal object DownloadAppUtils {
21 |
22 | const val KEY_OF_SP_APK_PATH = "KEY_OF_SP_APK_PATH"
23 |
24 | /**
25 | * apk 下载后本地文件路径
26 | */
27 | var downloadUpdateApkFilePath: String = ""
28 |
29 | /**
30 | * 更新信息
31 | */
32 | private val updateInfo by lazy { UpdateAppUtils.updateInfo }
33 |
34 | /**
35 | * context
36 | */
37 | private val context by lazy { globalContext()!! }
38 |
39 | /**
40 | * 是否在下载中
41 | */
42 | var isDownloading = false
43 |
44 | /**
45 | *下载进度回调
46 | */
47 | var onProgress: (Int) -> Unit = {}
48 |
49 | /**
50 | * 下载出错回调
51 | */
52 | var onError: () -> Unit = {}
53 |
54 | /**
55 | * 出错,点击重试回调
56 | */
57 | var onReDownload: () -> Unit = {}
58 |
59 | /**
60 | * 通过浏览器下载APK包
61 | */
62 | fun downloadForWebView(url: String) {
63 | val uri = Uri.parse(url)
64 | val intent = Intent(Intent.ACTION_VIEW, uri)
65 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
66 | context.startActivity(intent)
67 | }
68 |
69 | /**
70 | * 出错后,点击重试
71 | */
72 | fun reDownload() {
73 | onReDownload.invoke()
74 | download()
75 | }
76 |
77 | /**
78 | * App下载APK包,下载完成后安装
79 | */
80 | fun download() {
81 |
82 | (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED).no {
83 | log("没有SD卡")
84 | onError.invoke()
85 | return
86 | }
87 |
88 | var filePath = ""
89 | (updateInfo.config.apkSavePath.isNotEmpty()).yes {
90 | filePath = updateInfo.config.apkSavePath
91 | }.no {
92 | // 适配Android10
93 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !Environment.isExternalStorageLegacy()){
94 | filePath = (context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath ?: "") + "/apk"
95 | }else{
96 | val packageName = context.packageName
97 | filePath = Environment.getExternalStorageDirectory().absolutePath + "/" + packageName
98 | }
99 | }
100 |
101 | // apk 保存名称
102 | val apkName = if (updateInfo.config.apkSaveName.isNotEmpty()) {
103 | updateInfo.config.apkSaveName
104 | } else {
105 | context.appName
106 | }
107 |
108 | val apkLocalPath = "$filePath/$apkName.apk"
109 |
110 | downloadUpdateApkFilePath = apkLocalPath
111 |
112 | SPUtil.putBase(KEY_OF_SP_APK_PATH, downloadUpdateApkFilePath)
113 |
114 | FileDownloader.setup(context)
115 |
116 | val downloadTask = FileDownloader.getImpl().create(updateInfo.apkUrl)
117 | .setPath(apkLocalPath)
118 |
119 | downloadTask
120 | .addHeader("Accept-Encoding","identity")
121 | .addHeader("User-Agent","Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36")
122 | .setListener(object : FileDownloadLargeFileListener() {
123 |
124 | override fun pending(task: BaseDownloadTask, soFarBytes: Long, totalBytes: Long) {
125 | log("----使用FileDownloader下载-------")
126 | log("pending:soFarBytes($soFarBytes),totalBytes($totalBytes)")
127 | downloadStart()
128 | if(totalBytes < 0){
129 | downloadTask.pause()
130 | }
131 | }
132 |
133 | override fun progress(task: BaseDownloadTask, soFarBytes: Long, totalBytes: Long) {
134 | downloading(soFarBytes, totalBytes)
135 | if(totalBytes < 0){
136 | downloadTask.pause()
137 | }
138 | }
139 |
140 | override fun paused(task: BaseDownloadTask, soFarBytes: Long, totalBytes: Long) {
141 | log("获取文件总长度失败出错,尝试HTTPURLConnection下载")
142 | downloadUpdateApkFilePath.deleteFile()
143 | "$downloadUpdateApkFilePath.temp".deleteFile()
144 | downloadByHttpUrlConnection(filePath, apkName)
145 | }
146 |
147 | override fun completed(task: BaseDownloadTask) {
148 | downloadComplete()
149 | }
150 |
151 | override fun error(task: BaseDownloadTask, e: Throwable) {
152 | // FileDownloader 下载失败后,再调用 FileDownloadUtil 下载一次
153 | // FileDownloader 对码云或者阿里云上的apk文件会下载失败
154 | // downloadError(e)
155 | log("下载出错,尝试HTTPURLConnection下载")
156 | downloadUpdateApkFilePath.deleteFile()
157 | "$downloadUpdateApkFilePath.temp".deleteFile()
158 | downloadByHttpUrlConnection(filePath, apkName)
159 | }
160 |
161 | override fun warn(task: BaseDownloadTask) {
162 | }
163 | }).start()
164 | }
165 |
166 | /**
167 | * 使用 HttpUrlConnection 下载
168 | */
169 | private fun downloadByHttpUrlConnection(filePath: String, apkName: String?) {
170 | FileDownloadUtil.download(
171 | updateInfo.apkUrl,
172 | filePath,
173 | "$apkName.apk",
174 | onStart = { downloadStart() },
175 | onProgress = { current, total -> downloading(current, total) },
176 | onComplete = { downloadComplete() },
177 | onError = { downloadError(it) }
178 | )
179 | }
180 |
181 | /**
182 | * 开始下载逻辑
183 | */
184 | private fun downloadStart() {
185 | isDownloading = true
186 | UpdateAppUtils.downloadListener?.onStart()
187 | UpdateAppReceiver.send(context, 0)
188 | }
189 |
190 | /**
191 | * 下载中逻辑
192 | */
193 | private fun downloading(soFarBytes: Long, totalBytes: Long) {
194 | // log("soFarBytes:$soFarBytes--totalBytes:$totalBytes")
195 | isDownloading = true
196 | var progress = (soFarBytes * 100.0 / totalBytes).toInt()
197 | if (progress < 0) progress = 0
198 | log("progress:$progress")
199 | UpdateAppReceiver.send(context, progress)
200 | this@DownloadAppUtils.onProgress.invoke(progress)
201 | UpdateAppUtils.downloadListener?.onDownload(progress)
202 | }
203 |
204 | /**
205 | * 下载完成处理逻辑
206 | */
207 | private fun downloadComplete() {
208 | isDownloading = false
209 | log("completed")
210 | this@DownloadAppUtils.onProgress.invoke(100)
211 | UpdateAppUtils.downloadListener?.onFinish()
212 | // 校验md5
213 | (updateInfo.config.needCheckMd5).yes {
214 | checkMd5(context)
215 | }.no {
216 | UpdateAppReceiver.send(context, 100)
217 | }
218 | }
219 |
220 | /**
221 | * 下载失败处理逻辑
222 | */
223 | private fun downloadError(e: Throwable) {
224 | isDownloading = false
225 | log("error:${e.message}")
226 | downloadUpdateApkFilePath.deleteFile()
227 | this@DownloadAppUtils.onError.invoke()
228 | UpdateAppUtils.downloadListener?.onError(e)
229 | UpdateAppReceiver.send(context, -1000)
230 | }
231 |
232 | /**
233 | * 校验Md5
234 | * 先获取本应用的MD5值,获取未安装应用的MD5.进行对比
235 | */
236 | private fun checkMd5(context: Context) {
237 | // 当前应用md5
238 | val localMd5 = SignMd5Util.getAppSignatureMD5()
239 |
240 | // 下载的apk 签名md5
241 | val apkMd5 = SignMd5Util.getSignMD5FromApk(File(downloadUpdateApkFilePath))
242 | log("当前应用签名md5:$localMd5")
243 | log("下载apk签名md5:$apkMd5")
244 |
245 | // 校验结果回调
246 | UpdateAppUtils.md5CheckResultListener?.onResult(localMd5.equals(apkMd5, true))
247 |
248 | (localMd5.equals(apkMd5, true)).yes {
249 | log("md5校验成功")
250 | UpdateAppReceiver.send(context, 100)
251 | }.no {
252 | log("md5校验失败")
253 | }
254 | }
255 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/update/UpdateAppReceiver.kt:
--------------------------------------------------------------------------------
1 | package update
2 |
3 | import android.app.Notification
4 | import android.app.NotificationChannel
5 | import android.app.NotificationManager
6 | import android.app.PendingIntent
7 | import android.content.BroadcastReceiver
8 | import android.content.Context
9 | import android.content.Intent
10 | import android.graphics.BitmapFactory
11 | import android.os.Build
12 | import extension.installApk
13 | import extension.no
14 | import extension.yes
15 |
16 | /**
17 | * desc: UpdateAppReceiver
18 | * author: teprinciple on 2019/06/3.
19 | */
20 | internal class UpdateAppReceiver : BroadcastReceiver() {
21 |
22 | private val notificationChannel = "1001"
23 |
24 | private val updateConfig by lazy { UpdateAppUtils.updateInfo.config }
25 |
26 | private val uiConfig by lazy { UpdateAppUtils.updateInfo.uiConfig }
27 |
28 | private var lastProgress = 0
29 |
30 | override fun onReceive(context: Context, intent: Intent) {
31 |
32 | when (intent.action) {
33 |
34 | // 下载中
35 | context.packageName + ACTION_UPDATE -> {
36 | // 进度
37 | val progress = intent.getIntExtra(KEY_OF_INTENT_PROGRESS, 0)
38 |
39 | val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
40 |
41 | (progress != -1000).yes {
42 | lastProgress = progress
43 | }
44 |
45 | // 显示通知栏
46 | val notifyId = 1
47 | updateConfig.isShowNotification.yes {
48 | showNotification(context, notifyId, progress, notificationChannel, nm)
49 | }
50 |
51 | // 下载完成
52 | if (progress == 100) {
53 | handleDownloadComplete(context, notifyId, nm)
54 | }
55 | }
56 |
57 | // 重新下载
58 | context.packageName + ACTION_RE_DOWNLOAD -> {
59 | DownloadAppUtils.reDownload()
60 | }
61 | }
62 | }
63 |
64 | /**
65 | * 下载完成后的逻辑
66 | */
67 | private fun handleDownloadComplete(context: Context, notifyId: Int, nm: NotificationManager?) {
68 | // 关闭通知栏
69 | nm?.let {
70 | nm.cancel(notifyId)
71 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
72 | nm.deleteNotificationChannel(notificationChannel)
73 | }
74 | }
75 |
76 | // 安装apk
77 | context.installApk(DownloadAppUtils.downloadUpdateApkFilePath)
78 | }
79 |
80 | /**
81 | * 通知栏显示
82 | */
83 | private fun showNotification(context: Context, notifyId: Int, progress: Int, notificationChannel: String, nm: NotificationManager) {
84 |
85 | val notificationName = "notification"
86 |
87 | // 适配 8.0
88 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
89 | // 通知渠道
90 | val channel = NotificationChannel(notificationChannel, notificationName, NotificationManager.IMPORTANCE_HIGH)
91 | channel.enableLights(false)
92 | // 是否在桌面icon右上角展示小红点
93 | channel.setShowBadge(false)
94 | // 是否在久按桌面图标时显示此渠道的通知
95 | channel.enableVibration(false)
96 | // 最后在notificationmanager中创建该通知渠道
97 | nm.createNotificationChannel(channel)
98 | }
99 |
100 | val builder = Notification.Builder(context)
101 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
102 | builder.setChannelId(notificationChannel)
103 | }
104 |
105 |
106 | // 设置通知图标
107 | (updateConfig.notifyImgRes > 0).yes {
108 | builder.setSmallIcon(updateConfig.notifyImgRes)
109 | builder.setLargeIcon(BitmapFactory.decodeResource(context.resources, updateConfig.notifyImgRes))
110 | }.no {
111 | builder.setSmallIcon(android.R.mipmap.sym_def_app_icon)
112 | }
113 |
114 | // 设置进度
115 | builder.setProgress(100, lastProgress, false)
116 |
117 | if (progress == -1000) {
118 | val intent = Intent(context.packageName + ACTION_RE_DOWNLOAD)
119 | intent.setPackage(context.packageName)
120 | val pendingIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_CANCEL_CURRENT)
121 | builder.setContentIntent(pendingIntent)
122 | // 通知栏标题
123 | builder.setContentTitle(uiConfig.downloadFailText)
124 | } else {
125 | // 通知栏标题
126 | builder.setContentTitle("${uiConfig.downloadingBtnText}$progress%")
127 | }
128 |
129 |
130 | // 设置只响一次
131 | builder.setOnlyAlertOnce(true)
132 | val notification = builder.build()
133 | nm.notify(notifyId, notification)
134 | }
135 |
136 | companion object {
137 | /**
138 | * 进度key
139 | */
140 | private const val KEY_OF_INTENT_PROGRESS = "KEY_OF_INTENT_PROGRESS"
141 |
142 | /**
143 | * ACTION_UPDATE
144 | */
145 | const val ACTION_UPDATE = "teprinciple.update"
146 |
147 | /**
148 | * ACTION_RE_DOWNLOAD
149 | */
150 | const val ACTION_RE_DOWNLOAD = "action_re_download"
151 |
152 |
153 | const val REQUEST_CODE = 1001
154 |
155 |
156 | /**
157 | * 发送进度通知
158 | */
159 | fun send(context: Context, progress: Int) {
160 | val intent = Intent(context.packageName + ACTION_UPDATE)
161 | intent.putExtra(KEY_OF_INTENT_PROGRESS, progress)
162 | context.sendBroadcast(intent)
163 | }
164 | }
165 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/update/UpdateAppService.kt:
--------------------------------------------------------------------------------
1 | package update
2 |
3 | import android.app.Service
4 | import android.content.Intent
5 | import android.content.IntentFilter
6 | import android.os.IBinder
7 |
8 | /**
9 | * desc: UpdateAppService
10 | * author: teprinciple on 2018/11/3.
11 | */
12 | internal class UpdateAppService : Service() {
13 |
14 | private val updateAppReceiver = UpdateAppReceiver()
15 |
16 | override fun onCreate() {
17 | super.onCreate()
18 | // 动态注册receiver 适配8.0 updateAppReceiver 静态注册没收不到广播
19 | registerReceiver(updateAppReceiver, IntentFilter(packageName + UpdateAppReceiver.ACTION_UPDATE))
20 | registerReceiver(updateAppReceiver, IntentFilter(packageName + UpdateAppReceiver.ACTION_RE_DOWNLOAD))
21 | }
22 |
23 | override fun onDestroy() {
24 | super.onDestroy()
25 | unregisterReceiver(updateAppReceiver) // 注销广播
26 | }
27 |
28 | override fun onBind(intent: Intent): IBinder? {
29 | return null
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/updateapputils/src/main/java/update/UpdateAppUtils.kt:
--------------------------------------------------------------------------------
1 | package update
2 |
3 | import android.content.Context
4 | import extension.globalContext
5 | import extension.log
6 | import extension.no
7 | import extension.yes
8 | import listener.OnBtnClickListener
9 | import listener.Md5CheckResultListener
10 | import listener.OnInitUiListener
11 | import listener.UpdateDownloadListener
12 | import model.UiConfig
13 | import model.UpdateConfig
14 | import model.UpdateInfo
15 | import ui.UpdateAppActivity
16 | import util.GlobalContextProvider
17 | import util.SPUtil
18 |
19 |
20 | /**
21 | * Created by Teprinciple on 2016/11/15.
22 | */
23 | object UpdateAppUtils {
24 |
25 | // 更新信息对象
26 | internal val updateInfo by lazy { UpdateInfo() }
27 |
28 | // 下载监听
29 | internal var downloadListener: UpdateDownloadListener? = null
30 |
31 | // md5校验结果回调
32 | internal var md5CheckResultListener: Md5CheckResultListener? = null
33 |
34 | // 初始化更新弹窗UI回调
35 | internal var onInitUiListener: OnInitUiListener? = null
36 |
37 | // "暂不更新"按钮点击事件
38 | internal var onCancelBtnClickListener: OnBtnClickListener? = null
39 |
40 | // "立即更新"按钮点击事件
41 | internal var onUpdateBtnClickListener: OnBtnClickListener? = null
42 |
43 | /**
44 | * 设置apk下载地址
45 | */
46 | fun apkUrl(apkUrl: String): UpdateAppUtils {
47 | updateInfo.apkUrl = apkUrl
48 | return this
49 | }
50 |
51 | /**
52 | * 设置更新标题
53 | */
54 | fun updateTitle(title: CharSequence): UpdateAppUtils {
55 | updateInfo.updateTitle = title
56 | return this
57 | }
58 |
59 | /**
60 | * 设置更新内容
61 | */
62 | fun updateContent(content: CharSequence): UpdateAppUtils {
63 | updateInfo.updateContent = content
64 | return this
65 | }
66 |
67 | /**
68 | * 设置更新配置
69 | */
70 | fun updateConfig(config: UpdateConfig): UpdateAppUtils {
71 | updateInfo.config = config
72 | return this
73 | }
74 |
75 | /**
76 | * 设置UI配置
77 | */
78 | fun uiConfig(uiConfig: UiConfig): UpdateAppUtils {
79 | updateInfo.uiConfig = uiConfig
80 | return this
81 | }
82 |
83 | /**
84 | * 设置下载监听
85 | */
86 | fun setUpdateDownloadListener(listener: UpdateDownloadListener?): UpdateAppUtils {
87 | this.downloadListener = listener
88 | return this
89 | }
90 |
91 | /**
92 | * 设置md5校验结果监听
93 | */
94 | fun setMd5CheckResultListener(listener: Md5CheckResultListener?): UpdateAppUtils {
95 | this.md5CheckResultListener = listener
96 | return this
97 | }
98 |
99 | /**
100 | * 设置初始化UI监听
101 | */
102 | fun setOnInitUiListener(listener: OnInitUiListener?): UpdateAppUtils {
103 | this.onInitUiListener = listener
104 | return this
105 | }
106 |
107 | /**
108 | * 设置 “暂不更新” 按钮点击事件
109 | */
110 | fun setCancelBtnClickListener(listener: OnBtnClickListener?): UpdateAppUtils {
111 | this.onCancelBtnClickListener = listener
112 | return this
113 | }
114 |
115 | /**
116 | * 设置 “立即更新” 按钮点击事件
117 | */
118 | fun setUpdateBtnClickListener(listener: OnBtnClickListener?): UpdateAppUtils {
119 | this.onUpdateBtnClickListener = listener
120 | return this
121 | }
122 |
123 | /**
124 | * 检查更新
125 | */
126 | fun update() {
127 |
128 | if(globalContext() == null){
129 | log("请先调用初始化init")
130 | return
131 | }
132 |
133 | val keyName = (globalContext()?.packageName ?: "") + updateInfo.config.serverVersionName
134 | // 设置每次显示,设置本次显示及强制更新 每次都显示弹窗
135 | (updateInfo.config.alwaysShow || updateInfo.config.thisTimeShow || updateInfo.config.force).yes {
136 | UpdateAppActivity.launch()
137 | }.no {
138 | val hasShow = SPUtil.getBoolean(keyName, false)
139 | (hasShow).no { UpdateAppActivity.launch() }
140 | }
141 | SPUtil.putBase(keyName, true)
142 | }
143 |
144 | /* 未缓存apk
145 | /**
146 | * 删除已安装 apk
147 | */
148 | fun deleteInstalledApk() {
149 | val apkPath = SPUtil.getString(DownloadAppUtils.KEY_OF_SP_APK_PATH, "")
150 | val appVersionCode = Utils.getAPPVersionCode()
151 | val apkVersionCode = Utils.getApkVersionCode(apkPath)
152 | log("appVersionCode:$appVersionCode")
153 | log("apkVersionCode:$apkVersionCode")
154 | (apkPath.isNotEmpty() && appVersionCode == apkVersionCode && apkVersionCode > 0).yes {
155 | Utils.deleteFile(apkPath)
156 | }
157 | }
158 | */
159 |
160 | /**
161 | * 获取单例对象
162 | */
163 | @JvmStatic
164 | fun getInstance() = this
165 |
166 | /**
167 | * 初始化,非必须。解决部分手机 通过UpdateFileProvider 获取不到context情况使用
168 | * * @param context 提供全局context。
169 | */
170 | @JvmStatic
171 | fun init(context: Context){
172 | GlobalContextProvider.mContext = context.applicationContext
173 | log("外部初始化context")
174 | }
175 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/update/UpdateFileProvider.kt:
--------------------------------------------------------------------------------
1 | package update
2 |
3 | import android.support.v4.content.FileProvider
4 | import extension.log
5 | import extension.yes
6 | import util.GlobalContextProvider
7 |
8 | /**
9 | * desc: UpdateFileProvider
10 | * time: 2019/7/10
11 | * @author Teprinciple
12 | */
13 | class UpdateFileProvider : FileProvider() {
14 | override fun onCreate(): Boolean {
15 | val result = super.onCreate()
16 | (GlobalContextProvider.mContext == null && context != null).yes {
17 | GlobalContextProvider.mContext = context
18 | log("内部Provider初始化context:" + GlobalContextProvider.mContext)
19 | }
20 | return result
21 | }
22 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/util/AlertDialogUtil.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import android.app.Activity
4 | import android.app.AlertDialog
5 | import com.teprinciple.updateapputils.R
6 | import extension.string
7 |
8 | /**
9 | * desc: AlertDialogUtil
10 | * time: 2018/8/20
11 | * @author teprinciple
12 | */
13 | internal object AlertDialogUtil {
14 |
15 | fun show(
16 | activity: Activity,
17 | message: String,
18 | onCancelClick: () -> Unit = {},
19 | onSureClick: () -> Unit = {},
20 | cancelable: Boolean = false,
21 | title: String = string(R.string.notice),
22 | cancelText: String = string(R.string.cancel),
23 | sureText: String = string(R.string.sure)
24 | ) {
25 | AlertDialog.Builder(activity, R.style.AlertDialog)
26 | .setTitle(title)
27 | .setMessage(message)
28 | .setPositiveButton(sureText) { _, _ ->
29 | onSureClick.invoke()
30 | }
31 | .setNegativeButton(cancelText) { _, _ ->
32 | onCancelClick.invoke()
33 | }
34 | .setCancelable(cancelable)
35 | .create()
36 | .show()
37 | }
38 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/util/FileDownloadUtil.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import extension.log
4 | import extension.no
5 | import extension.yes
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.GlobalScope
8 | import kotlinx.coroutines.launch
9 | import java.io.File
10 | import java.io.FileOutputStream
11 | import java.io.InputStream
12 | import java.io.OutputStream
13 | import java.net.HttpURLConnection
14 | import java.net.HttpURLConnection.HTTP_OK
15 | import java.net.URL
16 |
17 | /**
18 | * desc: 文件下载 当 FileDownloader 对某些apk下载失败时(比如:放在阿里云,码云上apk) 使用该工具类下载
19 | * time: 2019/8/28
20 | * @author teprinciple
21 | */
22 | internal object FileDownloadUtil {
23 |
24 | /**
25 | * 下载文件
26 | * @param url 文件地址
27 | * @param fileSavePath 文件存储地址
28 | * @param fileName 文件存储名称
29 | * @param onStart 开始下载回调
30 | * @param onProgress 下载中回调
31 | * @param onComplete 下载完成回调
32 | * @param onError 下载失败回调
33 | */
34 | fun download(
35 | url: String,
36 | fileSavePath: String,
37 | fileName: String?,
38 | onStart: () -> Unit = {},
39 | onProgress: (current: Long, total: Long) -> Unit = { _, _ -> },
40 | onComplete: () -> Unit = {},
41 | onError: (Throwable) -> Unit = {}
42 | ) {
43 | GlobalScope.launch(Dispatchers.IO) {
44 | log("----使用HttpURLConnection下载----")
45 | onStart.invoke()
46 | var connection: HttpURLConnection? = null
47 | var outputStream: FileOutputStream? = null
48 |
49 | kotlin.runCatching {
50 | connection = URL(url).openConnection() as HttpURLConnection
51 | outputStream = FileOutputStream(File(fileSavePath, fileName))
52 |
53 | connection?.apply {
54 | requestMethod = "GET"
55 | setRequestProperty("Charset", "utf-8")
56 | setRequestProperty("Accept-Encoding", "identity")
57 | setRequestProperty("User-Agent", " Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.120 Safari/537.36")
58 | connect()
59 | }
60 |
61 | val responseCode = connection!!.responseCode
62 | if (responseCode == HTTP_OK) {
63 | val total = connection!!.contentLength
64 | var progress = -1
65 | connection!!.inputStream.use { input ->
66 | outputStream.use { output ->
67 | input.copyToWithProgress(output!!) {
68 | val pro = (it * 100.0 / total).toInt()
69 | (progress != pro).yes {
70 | GlobalScope.launch(Dispatchers.Main) {
71 | onProgress(it, total.toLong())
72 | }
73 | }
74 | progress = pro
75 | }
76 | }
77 | }
78 | }else{
79 | throw Throwable(message = "文件下载错误")
80 | }
81 | }.onSuccess {
82 | connection?.disconnect()
83 | outputStream?.close()
84 | log("HttpURLConnection下载完成")
85 | GlobalScope.launch(Dispatchers.Main) {
86 | (File(fileSavePath).length() > 0L).yes{
87 | onComplete.invoke()
88 | }.no {
89 | onError.invoke(Throwable(message = "文件下载错误"))
90 | }
91 | }
92 | }.onFailure {
93 | connection?.disconnect()
94 | outputStream?.close()
95 | log("HttpURLConnection下载失败:${it.message}")
96 | GlobalScope.launch(Dispatchers.Main) {
97 | onError.invoke(it)
98 | }
99 | }
100 | }
101 | }
102 | }
103 |
104 | fun InputStream.copyToWithProgress(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE, currentByte: (Long) -> Unit = {}): Long {
105 | var bytesCopied: Long = 0
106 | val buffer = ByteArray(bufferSize)
107 | var bytes = read(buffer)
108 | while (bytes >= 0) {
109 | out.write(buffer, 0, bytes)
110 | bytesCopied += bytes
111 | bytes = read(buffer)
112 | currentByte.invoke(bytesCopied)
113 | }
114 | return bytesCopied
115 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/util/GlobalContextProvider.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 |
6 | /**
7 | * desc: 提供context.
8 | */
9 | @SuppressLint("StaticFieldLeak")
10 | internal object GlobalContextProvider {
11 |
12 | /** 全局context 提供扩展globalContext */
13 | internal var mContext: Context? = null
14 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/util/SPUtil.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import android.app.Activity
4 | import android.content.SharedPreferences
5 | import extension.globalContext
6 |
7 | /**
8 | * SharedPreferences 数据保存
9 | */
10 | internal object SPUtil {
11 |
12 | fun putBase(keyName: String, value: Any): Boolean? {
13 | val sharedPreferences = getSp()
14 | val editor: SharedPreferences.Editor? = sharedPreferences?.edit()
15 | when (value) {
16 | is Int -> editor?.putInt(keyName, value)
17 | is Boolean -> editor?.putBoolean(keyName, value)
18 | is Float -> editor?.putFloat(keyName, value)
19 | is String -> editor?.putString(keyName, value)
20 | is Long -> editor?.putLong(keyName, value)
21 | else -> throw IllegalArgumentException("SharedPreferences can,t be save this type")
22 | }
23 | return editor?.commit()
24 | }
25 |
26 | fun getBoolean(keyName: String, defaultValue: Boolean = false): Boolean {
27 | val sharedPreferences = getSp()
28 | return sharedPreferences?.getBoolean(keyName, defaultValue) ?: false
29 | }
30 |
31 | fun getString(keyName: String, defaultValue: String? = null): String {
32 | val sharedPreferences = getSp()
33 | return sharedPreferences?.getString(keyName, defaultValue) ?: ""
34 | }
35 |
36 | private fun getSp(): SharedPreferences? {
37 | if (globalContext() == null) return null
38 | return globalContext()!!.getSharedPreferences(globalContext()!!.packageName, Activity.MODE_PRIVATE)
39 | }
40 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/java/util/SignMd5Util.kt:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import android.content.pm.PackageManager
4 | import android.content.pm.Signature
5 | import extension.globalContext
6 | import java.io.File
7 | import java.io.IOException
8 | import java.security.MessageDigest
9 | import java.security.NoSuchAlgorithmException
10 | import java.security.cert.Certificate
11 | import java.util.*
12 | import java.util.jar.JarEntry
13 | import java.util.jar.JarFile
14 | import kotlin.experimental.and
15 |
16 | /**
17 | * desc: 获取签名 md5
18 | * time: 2019/6/21
19 | * @author teprinciple
20 | */
21 | internal object SignMd5Util {
22 |
23 | private val HEX_DIGITS = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')
24 |
25 | /**
26 | * 获取当前应用签名文件md5
27 | */
28 | fun getAppSignatureMD5(): String {
29 | val packageName = globalContext()?.packageName ?: ""
30 | if (packageName.isEmpty()) return ""
31 | val signature = getAppSignature(packageName)
32 | return if (signature == null || signature.isEmpty()) {
33 | ""
34 | } else {
35 | bytes2HexString(hashTemplate(signature[0].toByteArray(), "MD5"))
36 | .replace("(?<=[0-9A-F]{2})[0-9A-F]{2}".toRegex(), ":$0")
37 | }
38 | }
39 |
40 | /**
41 | * 获取未安装apk 签名文件md5
42 | */
43 | fun getSignMD5FromApk(file: File): String {
44 | val signatures = ArrayList()
45 | val jarFile = JarFile(file)
46 | try {
47 | val je = jarFile.getJarEntry("AndroidManifest.xml")
48 | val readBuffer = ByteArray(8192)
49 | val certs = loadCertificates(jarFile, je, readBuffer)
50 | if (certs != null) {
51 | for (c in certs) {
52 | val sig = bytes2HexString(hashTemplate(c.encoded, "MD5"))
53 | .replace("(?<=[0-9A-F]{2})[0-9A-F]{2}".toRegex(), ":$0")
54 | signatures.add(sig)
55 | }
56 | }
57 | } catch (ex: Exception) {
58 | }
59 | return signatures.getOrNull(0) ?: ""
60 | }
61 |
62 | private fun getAppSignature(packageName: String): Array? {
63 | if (packageName.isEmpty()) return null
64 | return try {
65 | val pm = globalContext()?.packageManager
66 | val pi = pm?.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
67 | pi?.signatures
68 | } catch (e: Exception) {
69 | e.printStackTrace()
70 | null
71 | }
72 | }
73 |
74 | private fun hashTemplate(data: ByteArray?, algorithm: String): ByteArray? {
75 | if (data == null || data.isEmpty()) return null
76 | return try {
77 | val md = MessageDigest.getInstance(algorithm)
78 | md.update(data)
79 | md.digest()
80 | } catch (e: NoSuchAlgorithmException) {
81 | e.printStackTrace()
82 | null
83 | }
84 | }
85 |
86 | private fun bytes2HexString(bytes: ByteArray?): String {
87 | if (bytes == null) return ""
88 | val len = bytes.size
89 | if (len <= 0) return ""
90 | val ret = CharArray(len shl 1)
91 | var i = 0
92 | var j = 0
93 | while (i < len) {
94 | ret[j++] = HEX_DIGITS[bytes[i].toInt().shr(4) and 0x0f]
95 | ret[j++] = HEX_DIGITS[(bytes[i] and 0x0f).toInt()]
96 | i++
97 | }
98 | return String(ret)
99 | }
100 |
101 | /**
102 | * 加载签名
103 | */
104 | private fun loadCertificates(jarFile: JarFile, je: JarEntry?, readBuffer: ByteArray): Array? {
105 | try {
106 | val inputStream = jarFile.getInputStream(je)
107 | while (inputStream.read(readBuffer, 0, readBuffer.size) != -1) {
108 | }
109 | inputStream.close()
110 | return je?.certificates
111 | } catch (e: IOException) {
112 | }
113 | return null
114 | }
115 | }
--------------------------------------------------------------------------------
/updateapputils/src/main/res/anim/dialog_enter.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/updateapputils/src/main/res/anim/dialog_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/updateapputils/src/main/res/drawable/bg_update_btn.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/updateapputils/src/main/res/drawable/bg_update_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/updateapputils/src/main/res/drawable/ic_update_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/teprinciple/UpdateAppUtils/5afcb34f0d4a9bb11cec81119fdc1f14197a1114/updateapputils/src/main/res/drawable/ic_update_logo.png
--------------------------------------------------------------------------------
/updateapputils/src/main/res/layout/view_update_dialog_plentiful.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
20 |
21 |
32 |
33 |
42 |
43 |
55 |
56 |
57 |
74 |
75 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/updateapputils/src/main/res/layout/view_update_dialog_simple.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
21 |
22 |
28 |
29 |
40 |
41 |
42 |
46 |
47 |
51 |
52 |
61 |
62 |
67 |
68 |
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/updateapputils/src/main/res/values-en/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Notice
3 | Cancel
4 | OK
5 | Update
6 | Cancel
7 | New version!
8 | New version get ready,update now
9 | Download error, Click retry
10 | Start downloading...
11 | downloading
12 | Please allow access to storage permissions
13 | Current net type is not Wifi, Whether to continue
14 | Install
15 |
16 |
--------------------------------------------------------------------------------
/updateapputils/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #ffffff
4 | #0076FF
5 | #333333
6 | #555555
7 |
8 |
--------------------------------------------------------------------------------
/updateapputils/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | 提示
3 | 取消
4 | 确认
5 | 立即更新
6 | 暂不更新
7 | 版本更新啦!
8 | 发现新版本,立即更新
9 | 下载出错,点击重试
10 | 更新下载中...
11 | 下载中
12 | "暂无储存权限,是否前往打开"
13 | "当前没有连接Wifi,是否继续下载"
14 | 立即安装
15 |
16 |
--------------------------------------------------------------------------------
/updateapputils/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
13 |
14 |
30 |
31 |
35 |
--------------------------------------------------------------------------------
/updateapputils/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/updateapputils/src/main/res/xml/update_file_paths.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------