Full Code of teprinciple/UpdateAppDemo for AI

master 5afcb34f0d4a cached
75 files
148.8 KB
39.4k tokens
100 symbols
1 requests
Download .txt
Repository: teprinciple/UpdateAppDemo
Branch: master
Commit: 5afcb34f0d4a
Files: 75
Total size: 148.8 KB

Directory structure:
gitextract_os787tpu/

├── .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
│               ├── layout/
│               │   ├── activity_java_demo.xml
│               │   ├── activity_main.xml
│               │   ├── check_md5_demo_activity.xml
│               │   └── view_update_dialog_custom.xml
│               ├── values/
│               │   ├── colors.xml
│               │   ├── dimens.xml
│               │   ├── strings.xml
│               │   └── styles.xml
│               └── values-w820dp/
│                   └── dimens.xml
├── build.gradle
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── readme/
│   ├── README_1.5.2.md
│   ├── version.md
│   └── 自定义UI.md
├── settings.gradle
├── 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
                ├── layout/
                │   ├── view_update_dialog_plentiful.xml
                │   └── view_update_dialog_simple.xml
                ├── values/
                │   ├── colors.xml
                │   ├── strings.xml
                │   └── styles.xml
                ├── values-en/
                │   └── strings.xml
                └── xml/
                    ├── network_security_config.xml
                    └── update_file_paths.xml

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/android.yml
================================================
# workflow的名称,会显示在github 的项目的Actions的右边列表中,如下图
name: Android CI

# 在满足以下条件触发这个workflow
on:
  push:
    # 在指定的远程分支 master上,发生推送时
    branches: [ master ]

jobs:
  # 多个job,如果有多个,每个以“-”开头,这里只有一个 job
  build:

    runs-on: ubuntu-latest  # 该job 运行的系统环境,支持ubuntu 、windows、macOS

    # 下面是多个step ,每个以“-”开头
    steps:
      - uses: actions/checkout@v2       # step:检查分支
      - name: set up JDK 1.8            # step:设置jdk版本
        uses: actions/setup-java@v1     # 引用公共action
        with:
          java-version: 1.8
      - name: Build with Gradle         # step:打包apk
        # 运行打包命令
        run: chmod +x gradlew &&./gradlew assembleRelease

        #step:上传apk 到action,在右上角查看
        # 官方文档 https://help.github.com/cn/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts#uploading-build-and-test-artifacts
      - name: Upload APK
        uses: actions/upload-artifact@v1
        with:
            name: app
            path: app/build/outputs/apk/release/app-release.apk



================================================
FILE: .gitignore
================================================
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.externalNativeBuild


================================================
FILE: .idea/vcs.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
  <component name="VcsDirectoryMappings">
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
  </component>
</project>

================================================
FILE: README.md
================================================
# UpdateAppUtils2.0

 [ ![](https://img.shields.io/badge/platform-android-green.svg) ](http://developer.android.com/index.html) 
 [ ![Download](https://api.bintray.com/packages/teprinciple/maven/updateapputils/images/download.svg) ](https://bintray.com/teprinciple/maven/updateapputils/_latestVersion)

### 一行代码,快速实现app在线下载更新  A simple library for Android update app

#### UpdateAppUtils2.0 特点
* Kotlin First,Kotlin开发
* 支持AndroidX
* 支持Md5签名验证
* 支持自定义任意UI
* 适配中英文
* 适配至Android 10
* 通知栏图片自定义
* 支持修改是否每次显示弹窗(非强更)
* 支持安装完成后自动删除安装包

UpdateAppUtils2.0功能结构变化巨大,建议使用2.0以上版本;[2.0以前版本文档](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/README_1.5.2.md)

#### 效果图
<img src="https://github.com/teprinciple/UpdateAppUtils/blob/master/img/update_ui_simple.png" width="285"> <img src="https://github.com/teprinciple/UpdateAppUtils/blob/master/img/update_ui_plentiful.png" width="285"> <img src="https://github.com/teprinciple/UpdateAppUtils/blob/master/img/update_ui_change.png" width="285">
<img src="https://github.com/teprinciple/UpdateAppUtils/blob/master/img/update_ui_custom.png" width="285"> <img src="https://github.com/teprinciple/UpdateAppUtils/blob/master/img/update_ui_downloading.png" width="285"> <img src="https://github.com/teprinciple/UpdateAppUtils/blob/master/img/update_ui_fail.png" width="285">

### 集成

```
repositories {
   jcenter()
}
```

Support
```
implementation 'com.teprinciple:updateapputils:2.3.0'
```

AndroidX项目
```
注意,由于操作失误bintray 中updateapputilsX被我删掉,
所以2.3.0以后使用updateapputilsx。之前的仍使用updateapputilsX
//implementation 'com.teprinciple:updateapputilsX:2.2.1'
implementation 'com.teprinciple:updateapputilsx:2.3.0'

```

### 使用
下面为kotlin使用示例,Java示例请参考[JavaDemo](https://github.com/teprinciple/UpdateAppUtils/blob/master/app/src/main/java/com/example/teprinciple/updateappdemo/JavaDemoActivity.java)
#### 1、快速使用

##### 注意:部分手机SDK内部初始化不了context,造成context空指针,建议在application或者使用SDK前先初始化
```
 UpdateAppUtils.init(context)
```

```
 UpdateAppUtils
        .getInstance()
        .apkUrl(apkUrl)
        .updateTitle(updateTitle)
        .updateContent(updateContent)
        .update()
```

#### 2、多配置使用
```
    // ui配置
    val uiConfig = UiConfig().apply {
        uiType = UiType.PLENTIFUL
        cancelBtnText = "下次再说"
        updateLogoImgRes = R.drawable.ic_update
        updateBtnBgRes = R.drawable.bg_btn
        titleTextColor = Color.BLACK
        titleTextSize = 18f
        contentTextColor = Color.parseColor("#88e16531")
    }

    // 更新配置
    val updateConfig = UpdateConfig().apply {
        force = true
        checkWifi = true
        needCheckMd5 = true
        isShowNotification = true
        notifyImgRes = R.drawable.ic_logo
        apkSavePath = Environment.getExternalStorageDirectory().absolutePath +"/teprinciple"
        apkSaveName = "teprinciple"
    }

    UpdateAppUtils
        .getInstance()
        .apkUrl(apkUrl)
        .updateTitle(updateTitle)
        .updateContent(updateContent)
        .updateConfig(updateConfig)
        .uiConfig(uiConfig)
        .setUpdateDownloadListener(object : UpdateDownloadListener{
            // do something
        })
        .update()
```
#### 3、md5校验说明
 为了保证app的安全性,避免apk被二次打包的风险。UpdateAppUtils内部提供了对签名文件md5值校验的接口;
 首先你需要保证当前应用和服务器apk使用同一个签名文件进行了签名(一定要保管好自己的签名文件,否则检验就失去了意义),
 然后你需要将UpdateConfig 的 needCheckMd5 设置为true,并在Md5CheckResultListener监听中,监听校验返回结果。具体使用可参考
 [CheckMd5DemoActivity](https://github.com/teprinciple/UpdateAppUtils/blob/master/app/src/main/java/com/example/teprinciple/updateappdemo/CheckMd5DemoActivity.kt)
 ```
 UpdateAppUtils
        .getInstance()
        .apkUrl(apkUrl)
        .updateTitle(updateTitle)
        .updateContent(updateContent)
        .updateConfig(updateConfig) // needCheckMd5设置为true
        .setMd5CheckResultListener(object : Md5CheckResultListener{
            override fun onResult(result: Boolean) {
                // true:检验通过,false:检验失败
            }
        })
 ```

#### 4、自定义UI
 UpdateAppUtils内置了两套UI,你可以通过修改[UiConfig](#UiConfig)进行UI内容的自定义;
 当然当内部UI模板与你期望UI差别很大时,你可以采用[完全自定义UI](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/%E8%87%AA%E5%AE%9A%E4%B9%89UI.md)

### Api说明
#### 1、UpdateAppUtils Api

| api             | 说明                               | 默认值                   | 必须设置 |
|:-------------- |:------------------------------------ |:--------------------- |:------ |
| fun apkUrl(apkUrl: String)| 更新包服务器url         | null                  | true   |
| fun update() | UpdateAppUtils入口      | -     | true   |
| fun updateTitle(title: String)         | 更新标题     | 版本更新啦!     | false   |
| fun updateContent(content: String)        | 更新内容   | 发现新版本,立即更新  | false   |
| fun updateConfig(config: UpdateConfig)   | 更新配置  | 查看源码 | false  |
| fun uiConfig(uiConfig: UiConfig) | 更新弹窗UI配置  | 查看源码                 | false  |
| fun setUpdateDownloadListener() | 下载过程监听  | null | false   |
| fun setMd5CheckResultListener() | md5校验结果回调 | null  | false  |
| fun setOnInitUiListener() | 初始化更新弹窗UI回调 | null  | false  |
| fun deleteInstalledApk() | 删除已安装的apk | -  | false  |
| fun setCancelBtnClickListener() | 暂不更新按钮点击监听 | -  | false  |
| fun setUpdateBtnClickListener() | 立即更新按钮点击监听 | -  | false  |

#### 2、UpdateConfig:更新配置说明

| 属性                  | 说明                               | 默认值 |
|:--------------------- |:------------------------------------ |:------ |
| isDebug               | 是否输出【UpdateAppUtils】为Tag的日志|  true |
| force                 | 是否强制更新,强制时无取消按钮       | false  |
| apkSavePath           | apk下载存放位置               | 包名目录    |
| apkSaveName           | apk保存文件名                 | 项目名        |
| downloadBy            | 下载方式              | DownLoadBy.APP   |
| needCheckMd5          | 是否需要校验apk签名md5              | false   |
| checkWifi             | 检查是否wifi        | false   |
| isShowNotification    | 是否显示通知栏进度    | true   |
| notifyImgRes          | 通知栏图标              | 项目icon  |
| serverVersionName     | 服务器上apk版本名 | 无   |
| serverVersionCode     | 服务器上apk版本号 | 无   |
| alwaysShow            | 是否每次显示更新弹窗(非强更) | true   |
| thisTimeShow          | 本次是否显示更新弹窗(非强更) | false  |
| alwaysShowDownLoadDialog| 是否需要显示更新下载进度弹窗(非强更) | false  |
| showDownloadingToast  | 开始下载时是否显示Toast | true  |

#### 3、UiConfig:更新弹窗Ui配置说明 <div id = "UiConfig"/>

| 属性                  | 说明                               | 默认值 |
|:--------------------- |:------------------------------------ |:------ |
| uiType                | ui模板                        | UiType.SIMPLE |
| customLayoutId        | 自定义布局id    | false  |
| updateLogoImgRes      | 更新弹窗logo图片资源id               | -    |
| titleTextSize         | 标题字体大小                 | 16sp        |
| titleTextColor        | 标题字体颜色              | -   |
| contentTextSize         | 内容字体大小                 | 14sp       |
| contentTextColor        | 内容字体颜色              | -  |
| updateBtnBgColor      | 更新按钮背景颜色              | -   |
| updateBtnBgRes        | 更新按钮背景资源id        | -   |
| updateBtnTextColor    | 更新按钮字体颜色    | -   |
| updateBtnTextSize     | 更新按钮文字大小 | 14sp   |
| updateBtnText         | 更新按钮文案              | 立即更新  |
| cancelBtnBgColor      | 取消按钮背景颜色 | -   |
| cancelBtnBgRes        | 取消按钮背景资源id | -   |
| cancelBtnTextColor    | 取消按钮文字颜色 | -   |
| cancelBtnTextSize     | 取消按钮文字大小 | 14sp   |
| cancelBtnText         | 取消按钮文案 | 暂不更新   |
| downloadingToastText  | 开始下载时的Toast提示文字 | 更新下载中...   |
| downloadingBtnText    | 下载中 下载按钮以及通知栏标题前缀,进度自动拼接在后面 | 下载中   |
| downloadFailText      | 下载出错时,下载按钮及通知栏标题 | 下载出错,点击重试   |

### Demo体验
<img src="https://github.com/teprinciple/UpdateAppUtils/blob/master/img/demo.png" width="220">

### 更新日志

#### 2.3.0
* 修复部分手机context空指针异常
##### [更多历史版本](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/version.md)

================================================
FILE: app/.gitignore
================================================
/build


================================================
FILE: app/build.gradle
================================================
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    defaultConfig {
        applicationId "com.example.teprinciple.updateappdemo"
        minSdkVersion 19
        targetSdkVersion 29
        versionCode 203
        versionName "2.0.3"
    }
    signingConfigs {
        config {
            storeFile file('../update.jks')
            storePassword '123456'
            keyAlias 'update'
            keyPassword '123456'
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            signingConfig signingConfigs.debug
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }

        debug {
            minifyEnabled false
            signingConfig signingConfigs.config
        }
    }

    lintOptions {
        abortOnError false
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation project(':updateapputils')
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"

    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.1'
}


================================================
FILE: app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\Users\Teprinciple\AppData\Local\Android\Sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# Add any project specific keep options here:

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}


================================================
FILE: app/src/androidTest/java/com/example/teprinciple/updateappdemo/ExampleInstrumentedTest.java
================================================
package com.example.teprinciple.updateappdemo;

import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.*;

/**
 * Instrumentation test, which will execute on an Android device.
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
    @Test
    public void useAppContext() throws Exception {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getTargetContext();

        assertEquals("com.example.teprinciple.updateappdemo", appContext.getPackageName());
    }
}


================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.teprinciple.updateappdemo">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        tools:ignore="GoogleAppIndexingWarning">

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

        <activity android:name=".JavaDemoActivity"/>
        <activity android:name=".CheckMd5DemoActivity"/>

    </application>

</manifest>

================================================
FILE: app/src/main/java/com/example/teprinciple/updateappdemo/CheckMd5DemoActivity.kt
================================================
package com.example.teprinciple.updateappdemo

import android.os.Bundle
import android.os.Environment
import android.support.v7.app.AppCompatActivity
import android.widget.Toast
import kotlinx.android.synthetic.main.check_md5_demo_activity.*
import listener.Md5CheckResultListener
import model.UpdateConfig
import update.UpdateAppUtils

/**
 * desc: md5校验示例
 * time: 2019/7/1
 * @author yk
 */
class CheckMd5DemoActivity : AppCompatActivity() {

    /**
     * 已签名的apk
     */
    private val signedApkUrl = "http://118.24.148.250:8080/yk/update_signed.apk"

    /**
     * 非正规签名的apk
     */
    private val notSignedApkUrl = "http://118.24.148.250:8080/yk/update_not_signed.apk"

    private val updateTitle = "发现新版本V2.0.0"
    private val updateContent = "1、Kotlin重构版\n2、支持自定义UI\n3、增加md5校验\n4、更多功能等你探索"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.check_md5_demo_activity)

        // 更新配置
        val updateConfig = UpdateConfig().apply {
            force = true
            needCheckMd5 = true
        }

        // 正确签名
        btn_signed.setOnClickListener {
            updateConfig.apply { apkSaveName = "signed" }
            UpdateAppUtils
                .getInstance()
                .apkUrl(signedApkUrl)
                .updateTitle(updateTitle)
                .updateContent(updateContent)
                .updateConfig(updateConfig)
                .setMd5CheckResultListener(object : Md5CheckResultListener {
                    override fun onResult(result: Boolean) {
                        Toast.makeText(this@CheckMd5DemoActivity, "Md5检验是否通过:$result", Toast.LENGTH_SHORT).show()
                    }
                })
                .update()
        }

        // 错误签名
        btn_not_signed.setOnClickListener {
            updateConfig.apply { apkSaveName = "not_signed" }
            UpdateAppUtils
                .getInstance()
                .apkUrl(notSignedApkUrl)
                .updateTitle(updateTitle)
                .updateContent(updateContent)
                .updateConfig(updateConfig)
                .setMd5CheckResultListener(object : Md5CheckResultListener {
                    override fun onResult(result: Boolean) {
                        Toast.makeText(this@CheckMd5DemoActivity, "Md5检验是否通过:$result", Toast.LENGTH_SHORT).show()
                    }
                })
                .update()
        }
    }
}

================================================
FILE: app/src/main/java/com/example/teprinciple/updateappdemo/JavaDemoActivity.java
================================================
package com.example.teprinciple.updateappdemo;

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;

import org.jetbrains.annotations.NotNull;

import constant.UiType;
import listener.Md5CheckResultListener;
import listener.UpdateDownloadListener;
import model.UiConfig;
import model.UpdateConfig;
import update.UpdateAppUtils;

/**
 * desc: java使用实例
 * time: 2019/6/27
 * @author yk
 */
public class JavaDemoActivity extends AppCompatActivity {

    private String apkUrl = "http://118.24.148.250:8080/yk/update_signed.apk";
    private String updateTitle = "发现新版本V2.0.0";
    private String updateContent = "1、Kotlin重构版\n2、支持自定义UI\n3、增加md5校验\n4、更多功能等你探索";

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_java_demo);

        UpdateAppUtils.init(this);

        findViewById(R.id.btn_java).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                UpdateConfig updateConfig = new UpdateConfig();
                updateConfig.setCheckWifi(true);
                updateConfig.setNeedCheckMd5(true);
                updateConfig.setNotifyImgRes(R.drawable.ic_logo);

                UiConfig uiConfig = new UiConfig();
                uiConfig.setUiType(UiType.PLENTIFUL);

                UpdateAppUtils
                        .getInstance()
                        .apkUrl(apkUrl)
                        .updateTitle(updateTitle)
                        .updateContent(updateContent)
                        .uiConfig(uiConfig)
                        .updateConfig(updateConfig)
                        .setMd5CheckResultListener(new Md5CheckResultListener() {
                            @Override
                            public void onResult(boolean result) {

                            }
                        })
                        .setUpdateDownloadListener(new UpdateDownloadListener() {
                            @Override
                            public void onStart() {

                            }

                            @Override
                            public void onDownload(int progress) {

                            }

                            @Override
                            public void onFinish() {

                            }

                            @Override
                            public void onError(@NotNull Throwable e) {

                            }
                        })
                        .update();
            }
        });
    }
}


================================================
FILE: app/src/main/java/com/example/teprinciple/updateappdemo/MainActivity.kt
================================================
package com.example.teprinciple.updateappdemo

import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.os.Environment
import android.support.v7.app.AppCompatActivity
import android.view.View
import android.widget.TextView
import android.widget.Toast
import constant.DownLoadBy
import constant.UiType
import kotlinx.android.synthetic.main.activity_main.*
import listener.OnBtnClickListener
import listener.OnInitUiListener
import listener.UpdateDownloadListener
import model.UiConfig
import model.UpdateConfig
import update.UpdateAppUtils


class MainActivity : AppCompatActivity() {

    private val apkUrl = "http://118.24.148.250:8080/yk/update_signed.apk"
//    private val apkUrl = "https://github.com/AlexLiuSheng/CheckVersionLib/blob/master/library/src/main/java/com/allenliu/versionchecklib/utils/AppUtils.java"
    private val updateTitle = "发现新版本V2.0.0"
    private val updateContent = "1、Kotlin重构版\n2、支持自定义UI\n3、增加md5校验\n4、更多功能等你探索"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        UpdateAppUtils.init(this)

        // 基本使用
        btn_basic_use.setOnClickListener {
            UpdateAppUtils
                .getInstance()
                .apkUrl(apkUrl)
                .updateTitle(updateTitle)
                .updateConfig(UpdateConfig(apkSaveName = "up_1.1"))
                .uiConfig(UiConfig(uiType = UiType.SIMPLE))
                .updateContent(updateContent)
                .update()
        }

        // 浏览器下载
        btn_download_by_browser.setOnClickListener {

            // 使用SpannableString
            val content = SpanUtils(this)
                .appendLine("1、Kotlin重构版")
                .appendLine("2、支持自定义UI").setForegroundColor(Color.RED)
                .appendLine("3、增加md5校验").setForegroundColor(Color.parseColor("#88e16531")).setFontSize(20, true)
                .appendLine("4、更多功能等你探索").setBoldItalic()
                .appendLine().appendImage(R.mipmap.ic_launcher).setBoldItalic()
                .create()

            UpdateAppUtils
                .getInstance()
                .apkUrl(apkUrl)
                .updateTitle(updateTitle)
                .updateContent(content)
                .updateConfig(UpdateConfig().apply {
                    downloadBy = DownLoadBy.BROWSER
//                     alwaysShow = false
                    serverVersionName = "2.0.0"
                })
                .uiConfig(UiConfig(uiType = UiType.PLENTIFUL))

                // 设置 取消 按钮点击事件
                .setCancelBtnClickListener(object : OnBtnClickListener {
                    override fun onClick(): Boolean {
                        Toast.makeText(this@MainActivity, "cancel btn click", Toast.LENGTH_SHORT).show()
                        return false // 事件是否消费,是否需要传递下去。false-会执行原有点击逻辑,true-只执行本次设置的点击逻辑
                    }
                })

                // 设置 立即更新 按钮点击事件
                .setUpdateBtnClickListener(object : OnBtnClickListener {
                    override fun onClick(): Boolean {
                        Toast.makeText(this@MainActivity, "update btn click", Toast.LENGTH_SHORT).show()
                        return false // 事件是否消费,是否需要传递下去。false-会执行原有点击逻辑,true-只执行本次设置的点击逻辑
                    }
                })

                .update()
        }

        // 自定义UI
        btn_custom_ui.setOnClickListener {
            UpdateAppUtils
                .getInstance()
                .apkUrl(apkUrl)
                .updateTitle(updateTitle)
                .updateContent(updateContent)
                .updateConfig(UpdateConfig(alwaysShowDownLoadDialog = true))
                .uiConfig(UiConfig(uiType = UiType.CUSTOM, customLayoutId = R.layout.view_update_dialog_custom))
                .setOnInitUiListener(object : OnInitUiListener {
                    override fun onInitUpdateUi(view: View?, updateConfig: UpdateConfig, uiConfig: UiConfig) {
                        view?.findViewById<TextView>(R.id.tv_update_title)?.text = "版本更新啦"
                        view?.findViewById<TextView>(R.id.tv_version_name)?.text = "V2.0.0"
                        // do more...
                    }
                })
                .update()
        }

        // java使用示例
        btn_java_sample.setOnClickListener {
            startActivity(Intent(this, JavaDemoActivity::class.java))
        }

        // md5校验
        btn_check_md5.setOnClickListener {
            startActivity(Intent(this, CheckMd5DemoActivity::class.java))
        }

        // 高级使用
        btn_higher_level_use.setOnClickListener {
            // ui配置
            val uiConfig = UiConfig().apply {
                uiType = UiType.PLENTIFUL
                cancelBtnText = "下次再说"
                updateLogoImgRes = R.drawable.ic_update
                updateBtnBgRes = R.drawable.bg_btn
                titleTextColor = Color.BLACK
                titleTextSize = 18f
                contentTextColor = Color.parseColor("#88e16531")
            }

            // 更新配置
            val updateConfig = UpdateConfig().apply {
                force = true
                isDebug = true
                checkWifi = true
                isShowNotification = true
                notifyImgRes = R.drawable.ic_logo
                apkSavePath = Environment.getExternalStorageDirectory().absolutePath + "/teprinciple"
                apkSaveName = "teprinciple"
            }

            UpdateAppUtils
                .getInstance()
                .apkUrl(apkUrl)
                .updateTitle(updateTitle)
                .updateContent(updateContent)
                .updateConfig(updateConfig)
                .uiConfig(uiConfig)
                .setUpdateDownloadListener(object : UpdateDownloadListener {
                    override fun onStart() {
                    }

                    override fun onDownload(progress: Int) {
                    }

                    override fun onFinish() {
                    }

                    override fun onError(e: Throwable) {
                    }
                })
                .update()
        }
    }
}

================================================
FILE: app/src/main/java/com/example/teprinciple/updateappdemo/SpanUtils.java
================================================
package com.example.teprinciple.updateappdemo;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.BlurMaskFilter;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.annotation.ColorInt;
import android.support.annotation.DrawableRes;
import android.support.annotation.FloatRange;
import android.support.annotation.IntDef;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.text.Layout;
import android.text.Layout.Alignment;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.AlignmentSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.CharacterStyle;
import android.text.style.ClickableSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.LineHeightSpan;
import android.text.style.MaskFilterSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.ReplacementSpan;
import android.text.style.ScaleXSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.SubscriptSpan;
import android.text.style.SuperscriptSpan;
import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;
import android.text.style.UpdateAppearance;
import android.util.Log;

import java.io.InputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;

import static android.graphics.BlurMaskFilter.Blur;

/**
 * <pre>
 *     author: Blankj
 *     blog  : http://blankj.com
 *     usage : https://www.jianshu.com/p/509b0d2626f4
 *     time  : 16/12/13
 *     desc  : utils about span
 * </pre>
 */
public final class SpanUtils {

    private static final int COLOR_DEFAULT = 0xFEFFFFFF;

    public static final int ALIGN_BOTTOM = 0;
    public static final int ALIGN_BASELINE = 1;
    public static final int ALIGN_CENTER = 2;
    public static final int ALIGN_TOP = 3;

    @IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER, ALIGN_TOP})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Align {
    }

    private static final String LINE_SEPARATOR = System.getProperty("line.separator");

    private CharSequence mText;
    private int flag;
    private int foregroundColor;
    private int backgroundColor;
    private int lineHeight;
    private int alignLine;
    private int quoteColor;
    private int stripeWidth;
    private int quoteGapWidth;
    private int first;
    private int rest;
    private int bulletColor;
    private int bulletRadius;
    private int bulletGapWidth;
    private int fontSize;
    private boolean fontSizeIsDp;
    private float proportion;
    private float xProportion;
    private boolean isStrikethrough;
    private boolean isUnderline;
    private boolean isSuperscript;
    private boolean isSubscript;
    private boolean isBold;
    private boolean isItalic;
    private boolean isBoldItalic;
    private String fontFamily;
    private Typeface typeface;
    private Alignment alignment;
    private ClickableSpan clickSpan;
    private String url;
    private float blurRadius;
    private Blur style;
    private Shader shader;
    private float shadowRadius;
    private float shadowDx;
    private float shadowDy;
    private int shadowColor;
    private Object[] spans;

    private Bitmap imageBitmap;
    private Drawable imageDrawable;
    private Uri imageUri;
    private int imageResourceId;
    private int alignImage;

    private int spaceSize;
    private int spaceColor;

    private SpannableStringBuilder mBuilder;

    private int mType;
    private final int mTypeCharSequence = 0;
    private final int mTypeImage = 1;
    private final int mTypeSpace = 2;

    private Context mContext;

    public SpanUtils(Context context) {
        mContext = context.getApplicationContext();
        mBuilder = new SpannableStringBuilder();
        mText = "";
        setDefault();
    }

    private void setDefault() {
        flag = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;
        foregroundColor = COLOR_DEFAULT;
        backgroundColor = COLOR_DEFAULT;
        lineHeight = -1;
        quoteColor = COLOR_DEFAULT;
        first = -1;
        bulletColor = COLOR_DEFAULT;
        fontSize = -1;
        proportion = -1;
        xProportion = -1;
        isStrikethrough = false;
        isUnderline = false;
        isSuperscript = false;
        isSubscript = false;
        isBold = false;
        isItalic = false;
        isBoldItalic = false;
        fontFamily = null;
        typeface = null;
        alignment = null;
        clickSpan = null;
        url = null;
        blurRadius = -1;
        shader = null;
        shadowRadius = -1;
        spans = null;

        imageBitmap = null;
        imageDrawable = null;
        imageUri = null;
        imageResourceId = -1;

        spaceSize = -1;
    }

    /**
     * Set the span of flag.
     *
     * @param flag
     *         The flag.
     *         <ul>
     *         <li>{@link Spanned#SPAN_INCLUSIVE_EXCLUSIVE}</li>
     *         <li>{@link Spanned#SPAN_INCLUSIVE_INCLUSIVE}</li>
     *         <li>{@link Spanned#SPAN_EXCLUSIVE_EXCLUSIVE}</li>
     *         <li>{@link Spanned#SPAN_EXCLUSIVE_INCLUSIVE}</li>
     *         </ul>
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setFlag(final int flag) {
        this.flag = flag;
        return this;
    }

    /**
     * Set the span of foreground's color.
     *
     * @param color
     *         The color of foreground
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setForegroundColor(@ColorInt final int color) {
        this.foregroundColor = color;
        return this;
    }

    /**
     * Set the span of background's color.
     *
     * @param color
     *         The color of background
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setBackgroundColor(@ColorInt final int color) {
        this.backgroundColor = color;
        return this;
    }

    /**
     * Set the span of line height.
     *
     * @param lineHeight
     *         The line height, in pixel.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setLineHeight(@IntRange(from = 0) final int lineHeight) {
        return setLineHeight(lineHeight, ALIGN_CENTER);
    }

    /**
     * Set the span of line height.
     *
     * @param lineHeight
     *         The line height, in pixel.
     * @param align
     *         The alignment.
     *         <ul>
     *         <li>{@link Align#ALIGN_TOP   }</li>
     *         <li>{@link Align#ALIGN_CENTER}</li>
     *         <li>{@link Align#ALIGN_BOTTOM}</li>
     *         </ul>
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setLineHeight(@IntRange(from = 0) final int lineHeight,
                                   @Align final int align) {
        this.lineHeight = lineHeight;
        this.alignLine = align;
        return this;
    }

    /**
     * Set the span of quote's color.
     *
     * @param color
     *         The color of quote
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setQuoteColor(@ColorInt final int color) {
        return setQuoteColor(color, 2, 2);
    }

    /**
     * Set the span of quote's color.
     *
     * @param color
     *         The color of quote.
     * @param stripeWidth
     *         The width of stripe, in pixel.
     * @param gapWidth
     *         The width of gap, in pixel.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setQuoteColor(@ColorInt final int color,
                                   @IntRange(from = 1) final int stripeWidth,
                                   @IntRange(from = 0) final int gapWidth) {
        this.quoteColor = color;
        this.stripeWidth = stripeWidth;
        this.quoteGapWidth = gapWidth;
        return this;
    }

    /**
     * Set the span of leading margin.
     *
     * @param first
     *         The indent for the first line of the paragraph.
     * @param rest
     *         The indent for the remaining lines of the paragraph.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setLeadingMargin(@IntRange(from = 0) final int first,
                                      @IntRange(from = 0) final int rest) {
        this.first = first;
        this.rest = rest;
        return this;
    }

    /**
     * Set the span of bullet.
     *
     * @param gapWidth
     *         The width of gap, in pixel.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setBullet(@IntRange(from = 0) final int gapWidth) {
        return setBullet(0, 3, gapWidth);
    }

    /**
     * Set the span of bullet.
     *
     * @param color
     *         The color of bullet.
     * @param radius
     *         The radius of bullet, in pixel.
     * @param gapWidth
     *         The width of gap, in pixel.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setBullet(@ColorInt final int color,
                               @IntRange(from = 0) final int radius,
                               @IntRange(from = 0) final int gapWidth) {
        this.bulletColor = color;
        this.bulletRadius = radius;
        this.bulletGapWidth = gapWidth;
        return this;
    }

    /**
     * Set the span of font's size.
     *
     * @param size
     *         The size of font.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setFontSize(@IntRange(from = 0) final int size) {
        return setFontSize(size, false);
    }

    /**
     * Set the span of size of font.
     *
     * @param size
     *         The size of font.
     * @param isSp
     *         True to use sp, false to use pixel.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setFontSize(@IntRange(from = 0) final int size, final boolean isSp) {
        this.fontSize = size;
        this.fontSizeIsDp = isSp;
        return this;
    }

    /**
     * Set the span of proportion of font.
     *
     * @param proportion
     *         The proportion of font.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setFontProportion(final float proportion) {
        this.proportion = proportion;
        return this;
    }

    /**
     * Set the span of transverse proportion of font.
     *
     * @param proportion
     *         The transverse proportion of font.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setFontXProportion(final float proportion) {
        this.xProportion = proportion;
        return this;
    }

    /**
     * Set the span of strikethrough.
     *
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setStrikethrough() {
        this.isStrikethrough = true;
        return this;
    }

    /**
     * Set the span of underline.
     *
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setUnderline() {
        this.isUnderline = true;
        return this;
    }

    /**
     * Set the span of superscript.
     *
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setSuperscript() {
        this.isSuperscript = true;
        return this;
    }

    /**
     * Set the span of subscript.
     *
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setSubscript() {
        this.isSubscript = true;
        return this;
    }

    /**
     * Set the span of bold.
     *
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setBold() {
        isBold = true;
        return this;
    }

    /**
     * Set the span of bold.
     *
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setNotBold() {
        isBold = false;
        return this;
    }

    /**
     * Set the span of italic.
     *
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setItalic() {
        isItalic = true;
        return this;
    }

    /**
     * Set the span of bold italic.
     *
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setBoldItalic() {
        isBoldItalic = true;
        return this;
    }

    /**
     * Set the span of font family.
     *
     * @param fontFamily
     *         The font family.
     *         <ul>
     *         <li>monospace</li>
     *         <li>serif</li>
     *         <li>sans-serif</li>
     *         </ul>
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setFontFamily(@NonNull final String fontFamily) {
        this.fontFamily = fontFamily;
        return this;
    }

    /**
     * Set the span of typeface.
     *
     * @param typeface
     *         The typeface.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setTypeface(@NonNull final Typeface typeface) {
        this.typeface = typeface;
        return this;
    }

    /**
     * Set the span of alignment.
     *
     * @param alignment
     *         The alignment.
     *         <ul>
     *         <li>{@link Alignment#ALIGN_NORMAL  }</li>
     *         <li>{@link Alignment#ALIGN_OPPOSITE}</li>
     *         <li>{@link Alignment#ALIGN_CENTER  }</li>
     *         </ul>
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setAlign(@NonNull final Alignment alignment) {
        this.alignment = alignment;
        return this;
    }

    /**
     * Set the span of click.
     * <p>Must set {@code view.setMovementMethod(LinkMovementMethod.getInstance())}</p>
     *
     * @param clickSpan
     *         The span of click.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setClickSpan(@NonNull final ClickableSpan clickSpan) {
        this.clickSpan = clickSpan;
        return this;
    }

    /**
     * Set the span of url.
     * <p>Must set {@code view.setMovementMethod(LinkMovementMethod.getInstance())}</p>
     *
     * @param url
     *         The url.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setUrl(@NonNull final String url) {
        this.url = url;
        return this;
    }

    /**
     * Set the span of blur.
     *
     * @param radius
     *         The radius of blur.
     * @param style
     *         The style.
     *         <ul>
     *         <li>{@link Blur#NORMAL}</li>
     *         <li>{@link Blur#SOLID}</li>
     *         <li>{@link Blur#OUTER}</li>
     *         <li>{@link Blur#INNER}</li>
     *         </ul>
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setBlur(@FloatRange(from = 0, fromInclusive = false) final float radius,
                             final Blur style) {
        this.blurRadius = radius;
        this.style = style;
        return this;
    }

    /**
     * Set the span of shader.
     *
     * @param shader
     *         The shader.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setShader(@NonNull final Shader shader) {
        this.shader = shader;
        return this;
    }

    /**
     * Set the span of shadow.
     *
     * @param radius
     *         The radius of shadow.
     * @param dx
     *         X-axis offset, in pixel.
     * @param dy
     *         Y-axis offset, in pixel.
     * @param shadowColor
     *         The color of shadow.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setShadow(@FloatRange(from = 0, fromInclusive = false) final float radius,
                               final float dx,
                               final float dy,
                               final int shadowColor) {
        this.shadowRadius = radius;
        this.shadowDx = dx;
        this.shadowDy = dy;
        this.shadowColor = shadowColor;
        return this;
    }


    /**
     * Set the spans.
     *
     * @param spans
     *         The spans.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils setSpans(@NonNull final Object... spans) {
        if (spans.length > 0) {
            this.spans = spans;
        }
        return this;
    }

    /**
     * Append the text text.
     *
     * @param text
     *         The text.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils append(@NonNull final CharSequence text) {
        apply(mTypeCharSequence);
        mText = text;
        return this;
    }

    /**
     * Append one line.
     *
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils appendLine() {
        apply(mTypeCharSequence);
        mText = LINE_SEPARATOR;
        return this;
    }

    /**
     * Append text and one line.
     *
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils appendLine(@NonNull final CharSequence text) {
        apply(mTypeCharSequence);
        mText = text + LINE_SEPARATOR;
        return this;
    }

    /**
     * Append one image.
     *
     * @param bitmap
     *         The bitmap of image.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils appendImage(@NonNull final Bitmap bitmap) {
        return appendImage(bitmap, ALIGN_BOTTOM);
    }

    /**
     * Append one image.
     *
     * @param bitmap
     *         The bitmap.
     * @param align
     *         The alignment.
     *         <ul>
     *         <li>{@link Align#ALIGN_TOP     }</li>
     *         <li>{@link Align#ALIGN_CENTER  }</li>
     *         <li>{@link Align#ALIGN_BASELINE}</li>
     *         <li>{@link Align#ALIGN_BOTTOM  }</li>
     *         </ul>
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils appendImage(@NonNull final Bitmap bitmap, @Align final int align) {
        apply(mTypeImage);
        this.imageBitmap = bitmap;
        this.alignImage = align;
        return this;
    }

    /**
     * Append one image.
     *
     * @param drawable
     *         The drawable of image.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils appendImage(@NonNull final Drawable drawable) {
        return appendImage(drawable, ALIGN_BOTTOM);
    }

    /**
     * Append one image.
     *
     * @param drawable
     *         The drawable of image.
     * @param align
     *         The alignment.
     *         <ul>
     *         <li>{@link Align#ALIGN_TOP     }</li>
     *         <li>{@link Align#ALIGN_CENTER  }</li>
     *         <li>{@link Align#ALIGN_BASELINE}</li>
     *         <li>{@link Align#ALIGN_BOTTOM  }</li>
     *         </ul>
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils appendImage(@NonNull final Drawable drawable, @Align final int align) {
        apply(mTypeImage);
        this.imageDrawable = drawable;
        this.alignImage = align;
        return this;
    }

    /**
     * Append one image.
     *
     * @param uri
     *         The uri of image.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils appendImage(@NonNull final Uri uri) {
        return appendImage(uri, ALIGN_BOTTOM);
    }

    /**
     * Append one image.
     *
     * @param uri
     *         The uri of image.
     * @param align
     *         The alignment.
     *         <ul>
     *         <li>{@link Align#ALIGN_TOP     }</li>
     *         <li>{@link Align#ALIGN_CENTER  }</li>
     *         <li>{@link Align#ALIGN_BASELINE}</li>
     *         <li>{@link Align#ALIGN_BOTTOM  }</li>
     *         </ul>
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils appendImage(@NonNull final Uri uri, @Align final int align) {
        apply(mTypeImage);
        this.imageUri = uri;
        this.alignImage = align;
        return this;
    }

    /**
     * Append one image.
     *
     * @param resourceId
     *         The resource id of image.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils appendImage(@DrawableRes final int resourceId) {
        return appendImage(resourceId, ALIGN_BOTTOM);
    }

    /**
     * Append one image.
     *
     * @param resourceId
     *         The resource id of image.
     * @param align
     *         The alignment.
     *         <ul>
     *         <li>{@link Align#ALIGN_TOP     }</li>
     *         <li>{@link Align#ALIGN_CENTER  }</li>
     *         <li>{@link Align#ALIGN_BASELINE}</li>
     *         <li>{@link Align#ALIGN_BOTTOM  }</li>
     *         </ul>
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils appendImage(@DrawableRes final int resourceId, @Align final int align) {
        append(Character.toString((char) 0));// it's important for span start with image
        apply(mTypeImage);
        this.imageResourceId = resourceId;
        this.alignImage = align;
        return this;
    }

    /**
     * Append space.
     *
     * @param size
     *         The size of space.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils appendSpace(@IntRange(from = 0) final int size) {
        return appendSpace(size, Color.TRANSPARENT);
    }

    /**
     * Append space.
     *
     * @param size
     *         The size of space.
     * @param color
     *         The color of space.
     * @return the single {@link SpanUtils} instance
     */
    public SpanUtils appendSpace(@IntRange(from = 0) final int size, @ColorInt final int color) {
        apply(mTypeSpace);
        spaceSize = size;
        spaceColor = color;
        return this;
    }

    private void apply(final int type) {
        applyLast();
        mType = type;
    }

    /**
     * Create the span string.
     *
     * @return the span string
     */
    public SpannableStringBuilder create() {
        applyLast();
        return mBuilder;
    }

    private void applyLast() {
        if (mType == mTypeCharSequence) {
            updateCharCharSequence();
        } else if (mType == mTypeImage) {
            updateImage();
        } else if (mType == mTypeSpace) {
            updateSpace();
        }
        setDefault();
    }

    private void updateCharCharSequence() {
        if (mText.length() == 0) return;
        int start = mBuilder.length();
        mBuilder.append(mText);
        int end = mBuilder.length();
        if (foregroundColor != COLOR_DEFAULT) {
            mBuilder.setSpan(new ForegroundColorSpan(foregroundColor), start, end, flag);
        }
        if (backgroundColor != COLOR_DEFAULT) {
            mBuilder.setSpan(new BackgroundColorSpan(backgroundColor), start, end, flag);
        }
        if (first != -1) {
            mBuilder.setSpan(new LeadingMarginSpan.Standard(first, rest), start, end, flag);
        }
        if (quoteColor != COLOR_DEFAULT) {
            mBuilder.setSpan(
                    new CustomQuoteSpan(quoteColor, stripeWidth, quoteGapWidth),
                    start,
                    end,
                    flag
            );
        }
        if (bulletColor != COLOR_DEFAULT) {
            mBuilder.setSpan(
                    new CustomBulletSpan(bulletColor, bulletRadius, bulletGapWidth),
                    start,
                    end,
                    flag
            );
        }
//        if (imGapWidth != -1) {
//            if (imBitmap != null) {
//                mBuilder.setSpan(
//                        new CustomIconMarginSpan(imBitmap, imGapWidth, imAlign),
//                        start,
//                        end,
//                        flag
//                );
//            } else if (imDrawable != null) {
//                mBuilder.setSpan(
//                        new CustomIconMarginSpan(imDrawable, imGapWidth, imAlign),
//                        start,
//                        end,
//                        flag
//                );
//            } else if (imUri != null) {
//                mBuilder.setSpan(
//                        new CustomIconMarginSpan(imUri, imGapWidth, imAlign),
//                        start,
//                        end,
//                        flag
//                );
//            } else if (imResourceId != -1) {
//                mBuilder.setSpan(
//                        new CustomIconMarginSpan(imResourceId, imGapWidth, imAlign),
//                        start,
//                        end,
//                        flag
//                );
//            }
//        }
        if (fontSize != -1) {
            mBuilder.setSpan(new AbsoluteSizeSpan(fontSize, fontSizeIsDp), start, end, flag);
        }
        if (proportion != -1) {
            mBuilder.setSpan(new RelativeSizeSpan(proportion), start, end, flag);
        }
        if (xProportion != -1) {
            mBuilder.setSpan(new ScaleXSpan(xProportion), start, end, flag);
        }
        if (lineHeight != -1) {
            mBuilder.setSpan(new CustomLineHeightSpan(lineHeight, alignLine), start, end, flag);
        }
        if (isStrikethrough) {
            mBuilder.setSpan(new StrikethroughSpan(), start, end, flag);
        }
        if (isUnderline) {
            mBuilder.setSpan(new UnderlineSpan(), start, end, flag);
        }
        if (isSuperscript) {
            mBuilder.setSpan(new SuperscriptSpan(), start, end, flag);
        }
        if (isSubscript) {
            mBuilder.setSpan(new SubscriptSpan(), start, end, flag);
        }
        if (isBold) {
            mBuilder.setSpan(new StyleSpan(Typeface.BOLD), start, end, flag);
        }
        if (isItalic) {
            mBuilder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flag);
        }
        if (isBoldItalic) {
            mBuilder.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end, flag);
        }
        if (fontFamily != null) {
            mBuilder.setSpan(new TypefaceSpan(fontFamily), start, end, flag);
        }
        if (typeface != null) {
            mBuilder.setSpan(new CustomTypefaceSpan(typeface), start, end, flag);
        }
        if (alignment != null) {
            mBuilder.setSpan(new AlignmentSpan.Standard(alignment), start, end, flag);
        }
        if (clickSpan != null) {
            mBuilder.setSpan(clickSpan, start, end, flag);
        }
        if (url != null) {
            mBuilder.setSpan(new URLSpan(url), start, end, flag);
        }
        if (blurRadius != -1) {
            mBuilder.setSpan(
                    new MaskFilterSpan(new BlurMaskFilter(blurRadius, style)),
                    start,
                    end,
                    flag
            );
        }
        if (shader != null) {
            mBuilder.setSpan(new ShaderSpan(shader), start, end, flag);
        }
        if (shadowRadius != -1) {
            mBuilder.setSpan(
                    new ShadowSpan(shadowRadius, shadowDx, shadowDy, shadowColor),
                    start,
                    end,
                    flag
            );
        }
        if (spans != null) {
            for (Object span : spans) {
                mBuilder.setSpan(span, start, end, flag);
            }
        }
    }

    private void updateImage() {
        int start = mBuilder.length();
        mBuilder.append("<img>");
        int end = start + 5;
        if (imageBitmap != null) {
            mBuilder.setSpan(new CustomImageSpan(imageBitmap, alignImage), start, end, flag);
        } else if (imageDrawable != null) {
            mBuilder.setSpan(new CustomImageSpan(imageDrawable, alignImage), start, end, flag);
        } else if (imageUri != null) {
            mBuilder.setSpan(new CustomImageSpan(imageUri, alignImage), start, end, flag);
        } else if (imageResourceId != -1) {
            mBuilder.setSpan(new CustomImageSpan(imageResourceId, alignImage), start, end, flag);
        }
    }

    private void updateSpace() {
        int start = mBuilder.length();
        mBuilder.append("< >");
        int end = start + 3;
        mBuilder.setSpan(new SpaceSpan(spaceSize, spaceColor), start, end, flag);
    }

    class CustomLineHeightSpan extends CharacterStyle
            implements LineHeightSpan {

        private final int height;

        static final int ALIGN_CENTER = 2;

        static final int ALIGN_TOP = 3;

        final int mVerticalAlignment;

        CustomLineHeightSpan(int height, int verticalAlignment) {
            this.height = height;
            mVerticalAlignment = verticalAlignment;
        }

        @Override
        public void chooseHeight(final CharSequence text, final int start, final int end,
                                 final int spanstartv, final int v, final Paint.FontMetricsInt fm) {
            int need = height - (v + fm.descent - fm.ascent - spanstartv);
//            if (need > 0) {
            if (mVerticalAlignment == ALIGN_TOP) {
                fm.descent += need;
            } else if (mVerticalAlignment == ALIGN_CENTER) {
                fm.descent += need / 2;
                fm.ascent -= need / 2;
            } else {
                fm.ascent -= need;
            }
//            }
            need = height - (v + fm.bottom - fm.top - spanstartv);
//            if (need > 0) {
            if (mVerticalAlignment == ALIGN_TOP) {
                fm.top += need;
            } else if (mVerticalAlignment == ALIGN_CENTER) {
                fm.bottom += need / 2;
                fm.top -= need / 2;
            } else {
                fm.top -= need;
            }
//            }
        }

        @Override
        public void updateDrawState(final TextPaint tp) {

        }
    }

    class SpaceSpan extends ReplacementSpan {

        private final int width;
        private final int color;

        private SpaceSpan(final int width) {
            this(width, Color.TRANSPARENT);
        }

        private SpaceSpan(final int width, final int color) {
            super();
            this.width = width;
            this.color = color;
        }

        @Override
        public int getSize(@NonNull final Paint paint, final CharSequence text,
                           @IntRange(from = 0) final int start,
                           @IntRange(from = 0) final int end,
                           @Nullable final Paint.FontMetricsInt fm) {
            return width;
        }

        @Override
        public void draw(@NonNull final Canvas canvas, final CharSequence text,
                         @IntRange(from = 0) final int start,
                         @IntRange(from = 0) final int end,
                         final float x, final int top, final int y, final int bottom,
                         @NonNull final Paint paint) {
            Paint.Style style = paint.getStyle();
            int color = paint.getColor();

            paint.setStyle(Paint.Style.FILL);
            paint.setColor(this.color);

            canvas.drawRect(x, top, x + width, bottom, paint);

            paint.setStyle(style);
            paint.setColor(color);
        }
    }

    class CustomQuoteSpan implements LeadingMarginSpan {

        private final int color;
        private final int stripeWidth;
        private final int gapWidth;

        private CustomQuoteSpan(final int color, final int stripeWidth, final int gapWidth) {
            super();
            this.color = color;
            this.stripeWidth = stripeWidth;
            this.gapWidth = gapWidth;
        }

        public int getLeadingMargin(final boolean first) {
            return stripeWidth + gapWidth;
        }

        public void drawLeadingMargin(final Canvas c, final Paint p, final int x, final int dir,
                                      final int top, final int baseline, final int bottom,
                                      final CharSequence text, final int start, final int end,
                                      final boolean first, final Layout layout) {
            Paint.Style style = p.getStyle();
            int color = p.getColor();

            p.setStyle(Paint.Style.FILL);
            p.setColor(this.color);

            c.drawRect(x, top, x + dir * stripeWidth, bottom, p);

            p.setStyle(style);
            p.setColor(color);
        }
    }

    class CustomBulletSpan implements LeadingMarginSpan {

        private final int color;
        private final int radius;
        private final int gapWidth;

        private Path sBulletPath = null;

        private CustomBulletSpan(final int color, final int radius, final int gapWidth) {
            this.color = color;
            this.radius = radius;
            this.gapWidth = gapWidth;
        }

        public int getLeadingMargin(final boolean first) {
            return 2 * radius + gapWidth;
        }

        public void drawLeadingMargin(final Canvas c, final Paint p, final int x, final int dir,
                                      final int top, final int baseline, final int bottom,
                                      final CharSequence text, final int start, final int end,
                                      final boolean first, final Layout l) {
            if (((Spanned) text).getSpanStart(this) == start) {
                Paint.Style style = p.getStyle();
                int oldColor = 0;
                oldColor = p.getColor();
                p.setColor(color);
                p.setStyle(Paint.Style.FILL);
                if (c.isHardwareAccelerated()) {
                    if (sBulletPath == null) {
                        sBulletPath = new Path();
                        // Bullet is slightly better to avoid aliasing artifacts on mdpi devices.
                        sBulletPath.addCircle(0.0f, 0.0f, radius, Path.Direction.CW);
                    }
                    c.save();
                    c.translate(x + dir * radius, (top + bottom) / 2.0f);
                    c.drawPath(sBulletPath, p);
                    c.restore();
                } else {
                    c.drawCircle(x + dir * radius, (top + bottom) / 2.0f, radius, p);
                }
                p.setColor(oldColor);
                p.setStyle(style);
            }
        }
    }

    @SuppressLint("ParcelCreator")
    class CustomTypefaceSpan extends TypefaceSpan {

        private final Typeface newType;

        private CustomTypefaceSpan(final Typeface type) {
            super("");
            newType = type;
        }

        @Override
        public void updateDrawState(final TextPaint textPaint) {
            apply(textPaint, newType);
        }

        @Override
        public void updateMeasureState(final TextPaint paint) {
            apply(paint, newType);
        }

        private void apply(final Paint paint, final Typeface tf) {
            int oldStyle;
            Typeface old = paint.getTypeface();
            if (old == null) {
                oldStyle = 0;
            } else {
                oldStyle = old.getStyle();
            }

            int fake = oldStyle & ~tf.getStyle();
            if ((fake & Typeface.BOLD) != 0) {
                paint.setFakeBoldText(true);
            }

            if ((fake & Typeface.ITALIC) != 0) {
                paint.setTextSkewX(-0.25f);
            }

            paint.getShader();

            paint.setTypeface(tf);
        }
    }

    class CustomImageSpan extends CustomDynamicDrawableSpan {
        private Drawable mDrawable;
        private Uri mContentUri;
        private int mResourceId;

        private CustomImageSpan(final Bitmap b, final int verticalAlignment) {
            super(verticalAlignment);
            mDrawable = new BitmapDrawable(mContext.getResources(), b);
            mDrawable.setBounds(
                    0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()
            );
        }

        private CustomImageSpan(final Drawable d, final int verticalAlignment) {
            super(verticalAlignment);
            mDrawable = d;
            mDrawable.setBounds(
                    0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()
            );
        }

        private CustomImageSpan(final Uri uri, final int verticalAlignment) {
            super(verticalAlignment);
            mContentUri = uri;
        }

        private CustomImageSpan(@DrawableRes final int resourceId, final int verticalAlignment) {
            super(verticalAlignment);
            mResourceId = resourceId;
        }

        @Override
        public Drawable getDrawable() {
            Drawable drawable = null;
            if (mDrawable != null) {
                drawable = mDrawable;
            } else if (mContentUri != null) {
                Bitmap bitmap;
                try {
                    InputStream is =
                            mContext.getContentResolver().openInputStream(mContentUri);
                    bitmap = BitmapFactory.decodeStream(is);
                    drawable = new BitmapDrawable(mContext.getResources(), bitmap);
                    drawable.setBounds(
                            0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()
                    );
                    if (is != null) {
                        is.close();
                    }
                } catch (Exception e) {
                    Log.e("sms", "Failed to loaded content " + mContentUri, e);
                }
            } else {
                try {
                    drawable = ContextCompat.getDrawable(mContext, mResourceId);
                    drawable.setBounds(
                            0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()
                    );
                } catch (Exception e) {
                    Log.e("sms", "Unable to find resource: " + mResourceId);
                }
            }
            return drawable;
        }
    }

    abstract class CustomDynamicDrawableSpan extends ReplacementSpan {

        static final int ALIGN_BOTTOM = 0;

        static final int ALIGN_BASELINE = 1;

        static final int ALIGN_CENTER = 2;

        static final int ALIGN_TOP = 3;

        final int mVerticalAlignment;

        private CustomDynamicDrawableSpan() {
            mVerticalAlignment = ALIGN_BOTTOM;
        }

        private CustomDynamicDrawableSpan(final int verticalAlignment) {
            mVerticalAlignment = verticalAlignment;
        }

        public abstract Drawable getDrawable();

        @Override
        public int getSize(@NonNull final Paint paint, final CharSequence text,
                           final int start, final int end, final Paint.FontMetricsInt fm) {
            Drawable d = getCachedDrawable();
            Rect rect = d.getBounds();
            if (fm != null) {
//                LogUtils.d("fm.top: " + fm.top,
//                        "fm.ascent: " + fm.ascent,
//                        "fm.descent: " + fm.descent,
//                        "fm.bottom: " + fm.bottom,
//                        "lineHeight: " + (fm.bottom - fm.top));
                int lineHeight = fm.bottom - fm.top;
                if (lineHeight < rect.height()) {
                    if (mVerticalAlignment == ALIGN_TOP) {
                        fm.top = fm.top;
                        fm.bottom = rect.height() + fm.top;
                    } else if (mVerticalAlignment == ALIGN_CENTER) {
                        fm.top = -rect.height() / 2 - lineHeight / 4;
                        fm.bottom = rect.height() / 2 - lineHeight / 4;
                    } else {
                        fm.top = -rect.height() + fm.bottom;
                        fm.bottom = fm.bottom;
                    }
                    fm.ascent = fm.top;
                    fm.descent = fm.bottom;
                }
            }
            return rect.right;
        }

        @Override
        public void draw(@NonNull final Canvas canvas, final CharSequence text,
                         final int start, final int end, final float x,
                         final int top, final int y, final int bottom, @NonNull final Paint paint) {
            Drawable d = getCachedDrawable();
            Rect rect = d.getBounds();
            canvas.save();
            float transY;
            int lineHeight = bottom - top;
//            LogUtils.d("rectHeight: " + rect.height(),
//                    "lineHeight: " + (bottom - top));
            if (rect.height() < lineHeight) {
                if (mVerticalAlignment == ALIGN_TOP) {
                    transY = top;
                } else if (mVerticalAlignment == ALIGN_CENTER) {
                    transY = (bottom + top - rect.height()) / 2;
                } else if (mVerticalAlignment == ALIGN_BASELINE) {
                    transY = y - rect.height();
                } else {
                    transY = bottom - rect.height();
                }
                canvas.translate(x, transY);
            } else {
                canvas.translate(x, top);
            }
            d.draw(canvas);
            canvas.restore();
        }

        private Drawable getCachedDrawable() {
            WeakReference<Drawable> wr = mDrawableRef;
            Drawable d = null;
            if (wr != null) {
                d = wr.get();
            }
            if (d == null) {
                d = getDrawable();
                mDrawableRef = new WeakReference<>(d);
            }
            return d;
        }

        private WeakReference<Drawable> mDrawableRef;
    }

    class ShaderSpan extends CharacterStyle implements UpdateAppearance {
        private Shader mShader;

        private ShaderSpan(final Shader shader) {
            this.mShader = shader;
        }

        @Override
        public void updateDrawState(final TextPaint tp) {
            tp.setShader(mShader);
        }
    }

    class ShadowSpan extends CharacterStyle implements UpdateAppearance {
        private float radius;
        private float dx, dy;
        private int shadowColor;

        private ShadowSpan(final float radius,
                           final float dx,
                           final float dy,
                           final int shadowColor) {
            this.radius = radius;
            this.dx = dx;
            this.dy = dy;
            this.shadowColor = shadowColor;
        }

        @Override
        public void updateDrawState(final TextPaint tp) {
            tp.setShadowLayer(radius, dx, dy, shadowColor);
        }
    }
}

================================================
FILE: app/src/main/res/drawable/bg_btn.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="6dp"/>
    <solid android:color="#e16531"/>
</shape>

================================================
FILE: app/src/main/res/layout/activity_java_demo.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/btn_java"
        android:text="Java使用"
        android:layout_marginTop="50dp"
        android:layout_width="match_parent"
        android:layout_height="50dp"/>

</RelativeLayout>


================================================
FILE: app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="com.example.teprinciple.updateappdemo.MainActivity">

    <Button
        android:id="@+id/btn_basic_use"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:text="基本使用"/>

    <Button
        android:id="@+id/btn_download_by_browser"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="浏览器下载"/>

    <Button
        android:id="@+id/btn_higher_level_use"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="高级使用"/>

    <Button
        android:id="@+id/btn_custom_ui"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="自定义UI"/>


    <Button
        android:id="@+id/btn_check_md5"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="Md5校验"/>

    <Button
        android:id="@+id/btn_java_sample"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="Java"/>

</LinearLayout>


================================================
FILE: app/src/main/res/layout/check_md5_demo_activity.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:gravity="center"
        android:text="当前应用签名:F5:65:3A:FC:71:C6:AD:EE:BB:B2:D0:D4:67:73:8D:67"
        android:textSize="12sp"/>

    <Button
        android:id="@+id/btn_signed"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginTop="50dp"
        android:text="apk正确签名"/>

    <Button
        android:id="@+id/btn_not_signed"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginTop="10dp"
        android:text="apk错误签名"/>
</LinearLayout>

================================================
FILE: app/src/main/res/layout/view_update_dialog_custom.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="280dp"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <android.support.constraint.ConstraintLayout
        android:layout_width="280dp"
        android:layout_height="wrap_content"
        android:background="@drawable/bg_custom_update"
        android:paddingBottom="15dp">

        <TextView
            android:id="@+id/tv_update_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="130dp"
            android:textColor="@color/text_title"
            android:textSize="16sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.502"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="版本更新啦!"/>

        <ScrollView
            android:id="@+id/scrollView2"
            android:layout_width="match_parent"
            android:layout_height="90dp"
            android:layout_marginTop="10dp"
            android:overScrollMode="never"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/tv_update_title">

            <TextView
                android:id="@+id/tv_update_content"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_gravity="top"
                android:gravity="start"
                android:lineSpacingExtra="5dp"
                android:paddingLeft="20dp"
                android:paddingRight="20dp"
                android:textColor="@color/text_content"
                android:textSize="14sp"
                tools:text="1、快来升级最新版本\n2、这次更漂亮了\n3、快点来吧"/>
        </ScrollView>

        <TextView
            android:id="@+id/btn_update_sure"
            android:layout_width="0dp"
            android:layout_height="35dp"
            android:layout_marginStart="20dp"
            android:layout_marginTop="10dp"
            android:layout_marginEnd="20dp"
            android:background="@drawable/bg_update_btn"
            android:gravity="center"
            android:text="@string/update_now"
            android:textColor="@color/white"
            android:textSize="14sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/scrollView2"/>

        <TextView
            android:id="@+id/tv_version_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="40dp"
            android:layout_marginEnd="15dp"
            android:textColor="@color/white"
            android:textSize="14sp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="V1.0.0"/>

    </android.support.constraint.ConstraintLayout>

    <ImageView
        android:id="@+id/btn_update_cancel"
        android:layout_width="30dp"
        android:layout_height="30dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="20dp"
        android:src="@drawable/ic_close"/>

</LinearLayout>



================================================
FILE: app/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">#FF4081</color>
    <color name="text_blue">#0076FF</color>
    <color name="text_black">#333333</color>
</resources>


================================================
FILE: app/src/main/res/values/dimens.xml
================================================
<resources>
    <!-- Default screen margins, per the Android Design guidelines. -->
    <dimen name="activity_horizontal_margin">16dp</dimen>
    <dimen name="activity_vertical_margin">16dp</dimen>
</resources>


================================================
FILE: app/src/main/res/values/strings.xml
================================================
<resources>
    <string name="app_name">Update</string>
</resources>


================================================
FILE: app/src/main/res/values/styles.xml
================================================
<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style name="CustomDialog" parent="@android:style/Theme.Dialog">
        <item name="android:windowFrame">@null</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:windowIsTranslucent">false</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowBackground">@drawable/bg_update_dialog</item>
        <item name="android:backgroundDimEnabled">true</item>
    </style>

</resources>


================================================
FILE: app/src/main/res/values-w820dp/dimens.xml
================================================
<resources>
    <!-- Example customization of dimensions originally defined in res/values/dimens.update_file_paths
         (such as screen margins) for screens with more than 820dp of available width. This
         would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
    <dimen name="activity_horizontal_margin">64dp</dimen>
</resources>


================================================
FILE: build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    ext.kotlin_version = '1.3.31'

    repositories {
        jcenter()
        google()
        maven { url 'https://jitpack.io' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.4.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.novoda:bintray-release:0.9.1'
    }
}

allprojects {

    repositories {
        maven { url 'https://maven.aliyun.com/repository/public/' }
        maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' }
        maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' }
        maven { url 'http://maven.aliyun.com/nexus/content/repositories/gradle-plugin' }
        jcenter()
        google()
        maven { url 'https://jitpack.io' }
    }

    //中文注释
    tasks.withType(Javadoc) {
        options{ encoding "UTF-8"
            charSet 'UTF-8'
            links "http://docs.oracle.com/javase/7/docs/api"
        }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}




================================================
FILE: gradle/wrapper/gradle-wrapper.properties
================================================
#Mon Jun 03 12:27:34 CST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip


================================================
FILE: gradle.properties
================================================
# Project-wide Gradle settings.

# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.

# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html

# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m

# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true



================================================
FILE: gradlew
================================================
#!/usr/bin/env bash

##############################################################################
##
##  Gradle start up script for UN*X
##
##############################################################################

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""

APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

warn ( ) {
    echo "$*"
}

die ( ) {
    echo
    echo "$*"
    echo
    exit 1
}

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
  CYGWIN* )
    cygwin=true
    ;;
  Darwin* )
    darwin=true
    ;;
  MINGW* )
    msys=true
    ;;
esac

# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
    ls=`ls -ld "$PRG"`
    link=`expr "$ls" : '.*-> \(.*\)$'`
    if expr "$link" : '/.*' > /dev/null; then
        PRG="$link"
    else
        PRG=`dirname "$PRG"`"/$link"
    fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar

# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD="$JAVA_HOME/jre/sh/java"
    else
        JAVACMD="$JAVA_HOME/bin/java"
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD="java"
    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi

# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
    MAX_FD_LIMIT=`ulimit -H -n`
    if [ $? -eq 0 ] ; then
        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
            MAX_FD="$MAX_FD_LIMIT"
        fi
        ulimit -n $MAX_FD
        if [ $? -ne 0 ] ; then
            warn "Could not set maximum file descriptor limit: $MAX_FD"
        fi
    else
        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
    fi
fi

# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi

# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
    JAVACMD=`cygpath --unix "$JAVACMD"`

    # We build the pattern for arguments to be converted via cygpath
    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
    SEP=""
    for dir in $ROOTDIRSRAW ; do
        ROOTDIRS="$ROOTDIRS$SEP$dir"
        SEP="|"
    done
    OURCYGPATTERN="(^($ROOTDIRS))"
    # Add a user-defined pattern to the cygpath arguments
    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
    fi
    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    i=0
    for arg in "$@" ; do
        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option

        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
        else
            eval `echo args$i`="\"$arg\""
        fi
        i=$((i+1))
    done
    case $i in
        (0) set -- ;;
        (1) set -- "$args0" ;;
        (2) set -- "$args0" "$args1" ;;
        (3) set -- "$args0" "$args1" "$args2" ;;
        (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
        (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
        (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
        (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
        (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
        (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
    esac
fi

# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
    JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"

exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"


================================================
FILE: gradlew.bat
================================================
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=

set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto init

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:init
@rem Get command-line arguments, handling Windowz variants

if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args

:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2

:win9xME_args_slurp
if "x%~1" == "x" goto execute

set CMD_LINE_ARGS=%*
goto execute

:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar

@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%

:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega


================================================
FILE: readme/README_1.5.2.md
================================================
# updateapputils

### 一行代码,快速实现app在线下载更新  A simple library for Android update app
### 适配Android6.0、7.0、8.0
![](update.gif)

## 集成
compile引入
```
dependencies {
    implementation 'com.teprinciple:updateapputils:1.5.2'
}
```

## 使用
更新检测一般放在MainActivity或者启动页上,
在请求服务器版本检测接口获取到versionCode、versionName、最新apkPath后调用。

#### 快速使用
```
 UpdateAppUtils.from(this)
                .serverVersionCode(2)  //服务器versionCode
                .serverVersionName("2.0") //服务器versionName
                .apkPath(apkPath) //最新apk下载地址
                .update();
```
#### Kotlin代码调用完全一样
```
   private fun update() {
        val apkPath:String = "http://issuecdn.baidupcs.com/issue/netdisk/apk/BaiduNetdisk_7.15.1.apk"

        UpdateAppUtils.from(this)
                .serverVersionCode(2)
                .serverVersionName("2.0")
                .apkPath(apkPath)
                .update()
    }

```

#### 更多配置使用
```
UpdateAppUtils.from(this)
                .checkBy(UpdateAppUtils.CHECK_BY_VERSION_NAME) //更新检测方式,默认为VersionCode
                .serverVersionCode(2)
                .serverVersionName("2.0")
                .apkPath(apkPath)
                .showNotification(false) //是否显示下载进度到通知栏,默认为true
                .updateInfo(info)  //更新日志信息 String
                .downloadBy(UpdateAppUtils.DOWNLOAD_BY_BROWSER) //下载方式:app下载、手机浏览器下载。默认app下载
                .isForce(true) //是否强制更新,默认false 强制更新情况下用户不同意更新则不能使用app
                .update();
```

#### 说明
```
    1、UpdateAppUtils提供两种更新判断方式

    CHECK_BY_VERSION_CODE:通过versionCode判断,服务器上versionCode > 本地versionCode则执行更新

    CHECK_BY_VERSION_NAME:通过versionName判断,服务器上versionName 与 本地versionName不同则更新

    2、UpdateAppUtils提供两种下载apk方式

    DOWNLOAD_BY_APP:通过App下载

    DOWNLOAD_BY_BROWSER:通过手机浏览器下载

```

#### 关于适配Android6.0、7.0、8.0

库内部已经完全适配至8.0,你可以不用再对该库进行适配

#### 文章地址:[《UpdateAppUtils一行代码实现app在线更新》](http://www.jianshu.com/p/9c91bb984c85)

#### 更新日志
1.5.2<br>
修复部分bug
<br>1.5.1<br>
库内部适配至Android8.0
<br>1.4<br>
使用[filedownloader](https://github.com/lingochamp/FileDownloader)替换DownloadManager,避免部分手机DownLoadManager无效,同时解决了重复下载的问题,且提高了下载速度
增加接口UpdateAppUtils.needFitAndroidN(false),避免不需要适配7.0,也要设置FileProvider
<br>1.3.1<br>
修复部分bug,在demo中加入kotlin调用代码
<br>1.3<br>
增加接口方法 showNotification(false)//是否显示下载进度到通知栏;<br>updateInfo(info)//更新日志信息;下载前WiFi判断。
<br>1.2<br>
适配Android7.0,并在demo中加入适配6.0和7.0的代码
<br>1.1<br>
适配更多SdkVersion

================================================
FILE: readme/version.md
================================================
### 更新日志
#### 2.3.0
* 修复部分手机context空指针异常
#### 2.2.1
* 优化代码
* 修复部分bug
#### 2.2.0
* 适配Android 10
* 修复部分bug
#### 2.1.0
* 增加'暂不更新'按钮点击监听 setCancelBtnClickListener()
* 增加'立即更新'按钮点击监听 setUpdateBtnClickListener()
* 修复部分bug
#### 2.0.4
* 修复阿里云,码云平台上的apk FileDownloader下载失败
* 增加UpdateConfig alwaysShowDownLoadDialog字段,让非强更也能显示下载进度弹窗
#### 2.0.3
* 更新弹窗内容支持SpannableString
#### 2.0.2
* 9.0Http适配
#### 2.0.1
* 自定义FileProvide,防止provider冲突
#### 2.0.0
* Kotlin重构
* 支持AndroidX
* 安装包签名文件md5校验
* 通知栏自定义图标
* 支持自定义UI
* 适配中英文
* 增加下载回调等api
* 修复部分bug
#### 1.5.2
* 修复部分bug
#### 1.5.1
* 库内部适配至Android8.0
#### 1.4
* 使用[filedownloader](https://github.com/lingochamp/FileDownloader)替换DownloadManager,避免部分手机DownLoadManager无效,同时解决了重复下载的问题,且提高了下载速度
* 增加接口UpdateAppUtils.needFitAndroidN(false),避免不需要适配7.0,也要设置FileProvider
#### 1.3.1
* 修复部分bug,在demo中加入kotlin调用代码
#### 1.3
* 增加接口方法 showNotification(false) //是否显示下载进度到通知栏;
* updateInfo(info) //更新日志信息;
* 下载前WiFi判断。
#### 1.2
* 适配Android7.0,并在demo中加入适配6.0和7.0的代码
#### 1.1
* 适配更多SdkVersion

================================================
FILE: readme/自定义UI.md
================================================
## 完全自定义UI

### 1、创建你的layout(必须)
你可以创建任意你想要的UI布局([参考 view_update_dialog_custom.xml](https://github.com/teprinciple/UpdateAppUtils/blob/master/app/src/main/res/layout/view_update_dialog_custom.xml))
,但是控件id需要保持如下:

| id                  | 说明                 |      控件类型        | 是否必须 |
|:--------------------- |:-------------------|:----------------- |:------ |
| btn_update_sure       | 立即更新按钮id| 任意View |true |
| btn_update_cancel     | 暂不更新按钮id| 任意View  |true  |
| tv_update_title     | 更新弹窗标题| TextView |false  |
| tv_update_content     | 更新内容| TextView  |false  |

btn_update_sure和btn_update_cancel是必须提供的,否则更新无法继续;

tv_update_title,tv_update_content提供,UpdateAppUtils内部会自动
设置值,如果你不想这样,也可以自行命名,稍后通过OnInitUiListener接口进行相关文案设置;

### 2、注入到UpdateAppUtils(必须)

通过设置uiConfig,将自定义布局注入到UpdateAppUtils;注意uiType必须为UiType.CUSTOM

```
UpdateAppUtils
    .getInstance()
    // ...
    .uiConfig(UiConfig(uiType = UiType.CUSTOM, customLayoutId = R.layout.view_update_dialog_custom))
    .update()
```

### 3、实现OnInitUiListener接口(非必须)

UpdateAppUtils 中只对上表中的4个控件进行了相关内容的填充,如果你自定义的布局中有其他控件需要进行内容填充
需要实现OnInitUiListener接口来进行操作:
```
UpdateAppUtils
    .getInstance()
    // ...
    .setOnInitUiListener(object : OnInitUiListener {
        override fun onInitUpdateUi(view: View?, updateConfig: UpdateConfig, uiConfig: UiConfig) {
            view?.findViewById<TextView>(R.id.tv_update_title)?.text = "版本更新啦"
            view?.findViewById<TextView>(R.id.tv_version_name)?.text = "V2.0.0"
            // do more...
        }
    })
```

================================================
FILE: settings.gradle
================================================
include ':app', ':updateapputils'


================================================
FILE: updateapputils/.gitignore
================================================
/build


================================================
FILE: updateapputils/build.gradle
================================================
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-android'
apply plugin: 'com.novoda.bintray-release'

android {
    compileSdkVersion 29
    defaultConfig {
        minSdkVersion 19
        targetSdkVersion 29
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    // 忽略错误信息
    lintOptions {
        abortOnError false
    }

    androidExtensions {
        experimental = true
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.liulishuo.filedownloader:library:1.7.7'
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'
}

repositories {
    mavenCentral()
}

publish {
    userOrg = 'teprinciple'
    groupId = 'com.teprinciple'
    artifactId = 'updateapputils'
    publishVersion = '2.3.0'
    desc = 'A Simple library for Android update app'
    website = 'https://github.com/teprinciple/UpdateAppUtils'
}

================================================
FILE: updateapputils/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /Users/teprinciple/Library/Android/sdk/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# Add any project specific keep options here:

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile


================================================
FILE: updateapputils/src/androidTest/java/teprinciple/library/ExampleInstrumentedTest.java
================================================
package teprinciple.library;

import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.*;

/**
 * Instrumentation test, which will execute on an Android device.
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
    @Test
    public void useAppContext() throws Exception {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getTargetContext();

        assertEquals("teprinciple.library.test", appContext.getPackageName());
    }
}


================================================
FILE: updateapputils/src/main/AndroidManifest.xml
================================================
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:tools="http://schemas.android.com/tools"
          package="com.teprinciple.updateapputils">

    <uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>

    <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>

    <uses-permission android:name="android.permission.RESTART_PACKAGES"/>

    <application
        android:networkSecurityConfig="@xml/network_security_config"
        tools:ignore="UnusedAttribute">

        <activity
            android:name="ui.UpdateAppActivity"
            android:launchMode="singleTask"
            android:theme="@style/DialogActivityTheme"/>

        <service android:name="update.UpdateAppService"/>

        <provider
            android:name="update.UpdateFileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/update_file_paths"/>
        </provider>
    </application>

</manifest>


================================================
FILE: updateapputils/src/main/java/constant/DownLoadBy.kt
================================================
package constant

/**
 * desc: 下载方式
 * time: 2019/6/18
 * @author yk
 */
object DownLoadBy {
    /**
     * app下载
     */
    const val APP = 0x101

    /**
     * 浏览器下载
     */
    const val BROWSER = 0x102
}

================================================
FILE: updateapputils/src/main/java/constant/UiType.kt
================================================
package constant

/**
 * desc: UI 类型
 * time: 2019/6/27
 * @author yk
 */
object UiType {

    /**
     * 简洁版
     */
    const val SIMPLE = "SIMPLE"

    /**
     * 丰富版
     */
    const val PLENTIFUL = "PLENTIFUL"

    /**
     * 全自定义
     */
    const val CUSTOM = "CUSTOM"
}

================================================
FILE: updateapputils/src/main/java/extension/BooleanKtx.kt
================================================
package extension

import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract

@UseExperimental(ExperimentalContracts::class)
inline fun Boolean?.yes(block: () -> Unit): Boolean? {
    contract {
        callsInPlace(block, InvocationKind.AT_MOST_ONCE)
    }
    if (this == true) block()
    return this
}

@UseExperimental(ExperimentalContracts::class)
inline fun Boolean?.no(block: () -> Unit): Boolean? {
    contract {
        callsInPlace(block, InvocationKind.AT_MOST_ONCE)
    }
    if (this != true) block()
    return this
}

================================================
FILE: updateapputils/src/main/java/extension/ContextKtx.kt
================================================
package extension

import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.Uri
import android.os.Build
import android.support.v4.content.FileProvider
import java.io.File

/**
 * desc: context 相关扩展
 * author: teprinciple on 2020/3/27.
 */


/**
 * appName
 */
val Context.appName
    get() = packageManager.getPackageInfo(packageName, 0)?.applicationInfo?.loadLabel(packageManager).toString()

/**
 * 检测wifi是否连接
 */
fun Context.isWifiConnected(): Boolean {
    val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
    cm ?: return false
    val networkInfo = cm.activeNetworkInfo
    return networkInfo != null && networkInfo.type == ConnectivityManager.TYPE_WIFI
}


/**
 * 跳转安装
 */
fun Context.installApk(apkPath: String?) {

    if (apkPath.isNullOrEmpty())return

    val intent = Intent(Intent.ACTION_VIEW)
    val apkFile = File(apkPath)

    // android 7.0 fileprovider 适配
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
        val contentUri = FileProvider.getUriForFile(this, this.packageName + ".fileprovider", apkFile)
        intent.setDataAndType(contentUri, "application/vnd.android.package-archive")
    } else {
        intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive")
    }

    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    this.startActivity(intent)
}




================================================
FILE: updateapputils/src/main/java/extension/CoreKtx.kt
================================================
package extension

import android.app.ActivityManager
import android.content.Context
import android.os.Build
import android.support.v4.content.ContextCompat
import android.util.Log
import android.view.View
import update.UpdateAppUtils
import util.GlobalContextProvider
import kotlin.system.exitProcess

/**
 * desc: 扩展
 * author: teprinc
 * iple on 2020/3/27.
 */

/**
 * 全局context
 */
fun globalContext() = GlobalContextProvider.mContext


/**
 * 打印日志
 */
fun log(content: String?) = UpdateAppUtils.updateInfo.config.isDebug.yes {
    Log.e("[UpdateAppUtils]", content ?: "")
}

/**
 * 获取color
 */
fun color(color: Int) = if (globalContext() == null) 0 else ContextCompat.getColor(globalContext()!!, color)

/**
 * 获取 String
 */
fun string(string: Int) = globalContext()?.getString(string) ?: ""

/**
 * view 显示隐藏
 */
fun View.visibleOrGone(show: Boolean) {
    if (show) {
        this.visibility = View.VISIBLE
    } else {
        this.visibility = View.GONE
    }
}

/**
 * 退出app
 */
fun exitApp() {
    val manager = globalContext()!!.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        manager.appTasks.forEach { it.finishAndRemoveTask() }
    } else {
        exitProcess(0)
    }
}

================================================
FILE: updateapputils/src/main/java/extension/StringKtx.kt
================================================
package extension

import java.io.File

/**
 * desc: string 相关扩展
 * author: teprinciple on 2020/3/27.
 */

/**
 * 根据文件路径删除文件
 */
fun String?.deleteFile() {
    kotlin.runCatching {
        val file = File(this ?: "")
        (file.isFile).yes {
            file.delete()
            log("删除成功")
        }
    }.onFailure {
        log(it.message)
    }
}

================================================
FILE: updateapputils/src/main/java/listener/Md5CheckResultListener.kt
================================================
package listener

/**
 * desc: Md5校验结果回调
 * time: 2019/6/21
 * @author teprinciple
 */
interface Md5CheckResultListener {
    fun onResult(result: Boolean)
}

================================================
FILE: updateapputils/src/main/java/listener/OnBtnClickListener.kt
================================================
package listener

/**
 * desc: 按钮点击监听
 * time: 2019/9/16
 * @author teprinciple
 */
interface OnBtnClickListener {

    /**
     * 按钮点击
     * @return 是否消费事件
     */
    fun onClick(): Boolean
}

================================================
FILE: updateapputils/src/main/java/listener/OnInitUiListener.kt
================================================
package listener

import android.view.View
import model.UiConfig
import model.UpdateConfig

/**
 * desc: 初始化UI 回调 用于进一步自定义UI
 * time: 2019/6/28
 * @author teprinciple
 */
interface OnInitUiListener {

    /**
     * 初始化更新弹窗回调
     * @param view 弹窗view
     * @param updateConfig 当前更新配置
     * @param uiConfig 当前ui配置
     */
    fun onInitUpdateUi(view: View?, updateConfig: UpdateConfig, uiConfig: UiConfig)
}

================================================
FILE: updateapputils/src/main/java/listener/UpdateDownloadListener.kt
================================================
package listener

/**
 * desc: 下载监听
 * time: 2019/6/19
 * @author teprinciple
 */
interface UpdateDownloadListener {

    /**
     * 开始下载
     */
    fun onStart()

    /**
     * 下载中
     * @param progress 进度 0 - 100
     */
    fun onDownload(progress: Int)

    /**
     * 下载完成
     */
    fun onFinish()

    /**
     * 下载错误
     */
    fun onError(e: Throwable)
}

================================================
FILE: updateapputils/src/main/java/model/UiConfig.kt
================================================
package model

import com.teprinciple.updateapputils.R
import constant.UiType
import extension.string

/**
 * desc: UiConfig UI 配置
 * time: 2019/6/27
 * @author teprinciple
 */
data class UiConfig(
    // ui类型,默认简洁版
    var uiType: String = UiType.SIMPLE,
    // 自定义UI 布局id
    var customLayoutId: Int? = null,
    // 更新弹窗中的logo
    var updateLogoImgRes: Int? = null,
    // 标题相关设置
    var titleTextSize: Float? = null,
    var titleTextColor: Int? = null,
    // 更新内容相关设置
    var contentTextSize: Float? = null,
    var contentTextColor: Int? = null,
    // 更新按钮相关设置
    var updateBtnBgColor: Int? = null,
    var updateBtnBgRes: Int? = null,
    var updateBtnTextColor: Int? = null,
    var updateBtnTextSize: Float? = null,
    var updateBtnText: CharSequence = string(R.string.update_now),
    // 取消按钮相关设置
    var cancelBtnBgColor: Int? = null,
    var cancelBtnBgRes: Int? = null,
    var cancelBtnTextColor: Int? = null,
    var cancelBtnTextSize: Float? = null,
    var cancelBtnText: CharSequence = string(R.string.update_cancel),

    // 开始下载时的Toast提示文字
    var downloadingToastText: CharSequence = string(R.string.toast_download_apk),
    // 下载中 下载按钮以及通知栏标题前缀,进度自动拼接在后面
    var downloadingBtnText: CharSequence = string(R.string.downloading),
    // 下载出错时,下载按钮及通知栏标题
    var downloadFailText: CharSequence = string(R.string.download_fail)
)

================================================
FILE: updateapputils/src/main/java/model/UpdateConfig.kt
================================================
package model

import constant.DownLoadBy

data class UpdateConfig(
    var isDebug: Boolean = true, // 是否是调试模式,调试模式会输出日志

    var alwaysShow: Boolean = true, // 非强制更新时,是否每次都显示弹窗,用VersionName来判断
    var thisTimeShow: Boolean = false, // 非强制更新时,指定本次显示弹窗
    var alwaysShowDownLoadDialog: Boolean = false, // 非强制更新时,也显示下载进度dialog
    var force: Boolean = false, // 是否强制更新
    var apkSavePath: String = "", // apk下载存放位置
    var apkSaveName: String = "", // apk 保存名(默认是app的名字)
    var downloadBy: Int = DownLoadBy.APP, // 下载方式:默认app下载
    //var downloadDirect: Boolean = false, // 不需要弹窗,直接开始下载安装
    var checkWifi: Boolean = false, // 是否检查是否wifi
    var isShowNotification: Boolean = true, // 是否在通知栏显示
    var notifyImgRes: Int = 0, // 通知栏图标
    var needCheckMd5: Boolean = false, // 是否需要进行md5校验,仅app下载方式有效
    var showDownloadingToast: Boolean = true, // 是否需要显示 【更新下载中】文案
    var serverVersionName: String = "", // 服务器上版本名
    var serverVersionCode: Int = 0 // 服务器上版本号
)

================================================
FILE: updateapputils/src/main/java/model/UpdateInfo.kt
================================================
package model

import com.teprinciple.updateapputils.R
import extension.string

/**
 * desc: UpdateInfo
 * time: 2019/6/18
 * @author teprinciple
 */
internal data class UpdateInfo(
    // 更新标题
    var updateTitle: CharSequence = string(R.string.update_title),
    // 更新内容
    var updateContent: CharSequence = string(R.string.update_content),
    // apk 下载地址
    var apkUrl: String = "",
    // 更新配置
    var config: UpdateConfig = UpdateConfig(),
    // ui配置
    var uiConfig: UiConfig = UiConfig()
)

================================================
FILE: updateapputils/src/main/java/ui/UpdateAppActivity.kt
================================================
package ui

import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.support.v4.app.ActivityCompat
import android.support.v4.content.ContextCompat
import android.support.v7.app.AppCompatActivity
import android.view.MotionEvent
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import com.teprinciple.updateapputils.R
import constant.DownLoadBy
import constant.UiType
import extension.*
import update.DownloadAppUtils
import update.UpdateAppService
import update.UpdateAppUtils
import util.AlertDialogUtil
import util.GlobalContextProvider
import util.SPUtil

/**
 * desc: 更新弹窗
 * author: teprinciple on 2019/06/3.
 */
internal class UpdateAppActivity : AppCompatActivity() {

    private var tvTitle: TextView? = null
    private var tvContent: TextView? = null
    private var sureBtn: View? = null
    private var cancelBtn: View? = null
    private var ivLogo: ImageView? = null

    /**
     * 更新信息
     */
    private val updateInfo by lazy { UpdateAppUtils.updateInfo }

    /**
     * 更新配置
     */
    private val updateConfig by lazy { updateInfo.config }

    /**
     * ui 配置
     */
    private val uiConfig by lazy { updateInfo.uiConfig }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        if (GlobalContextProvider.mContext == null){
            GlobalContextProvider.mContext = this.applicationContext
        }

        setContentView(
            when (uiConfig.uiType) {
                UiType.SIMPLE -> R.layout.view_update_dialog_simple
                UiType.PLENTIFUL -> R.layout.view_update_dialog_plentiful
                UiType.CUSTOM -> uiConfig.customLayoutId ?: R.layout.view_update_dialog_simple
                else -> R.layout.view_update_dialog_simple
            }
        )
        initView()
        initUi()

        // 初始化UI回调,用于进一步自定义UI
        UpdateAppUtils.onInitUiListener?.onInitUpdateUi(
            window.decorView.findViewById(android.R.id.content),
            updateConfig,
            uiConfig)

        // 每次弹窗后,下载前均把本地之前缓存的apk删除,避免缓存老版本apk或者问题apk,并不重新下载新的apk
        SPUtil.getString(DownloadAppUtils.KEY_OF_SP_APK_PATH, "").deleteFile()
    }

    @SuppressLint("ClickableViewAccessibility")
    private fun initView() {

        tvTitle = findViewById(R.id.tv_update_title)
        tvContent = findViewById(R.id.tv_update_content)
        cancelBtn = findViewById(R.id.btn_update_cancel)
        sureBtn = findViewById(R.id.btn_update_sure)
        ivLogo = findViewById(R.id.iv_update_logo)

        // 更新标题
        tvTitle?.text = updateInfo.updateTitle

        // 更新内容
        tvContent?.text = updateInfo.updateContent

        // 取消
        cancelBtn?.setOnClickListener {
            updateConfig.force.yes {
                exitApp()
            }.no {
                finish()
            }
        }

        // 确定
        sureBtn?.setOnClickListener {

            DownloadAppUtils.isDownloading.no {
                if (sureBtn is TextView) {
                    (sureBtn as? TextView)?.text = uiConfig.updateBtnText
                }
                preDownLoad()
            }
        }

        // 显示或隐藏取消按钮, 强更时默认不显示取消按钮
        hideShowCancelBtn(!updateConfig.force)

        // 外部额外设置 取消 按钮点击事件
        cancelBtn?.setOnTouchListener { v, event ->
            when (event.action) {
                MotionEvent.ACTION_UP -> {
                    UpdateAppUtils.onCancelBtnClickListener?.onClick() ?: false
                }
                else -> false
            }
        }

        // 外部额外设置 立即更新 按钮点击事件
        sureBtn?.setOnTouchListener { v, event ->
            when (event.action) {
                MotionEvent.ACTION_UP -> {
                    UpdateAppUtils.onUpdateBtnClickListener?.onClick() ?: false
                }
                else -> false
            }
        }
    }

    /**
     * 取消按钮处理
     */
    private fun hideShowCancelBtn(show: Boolean) {
        // 强制更新 不显示取消按钮
        cancelBtn?.visibleOrGone(show)
        // 取消按钮与确定按钮中的间隔线
        findViewById<View>(R.id.view_line)?.visibleOrGone(show)
    }

    /**
     * 初始化UI
     */
    private fun initUi() {

        uiConfig.apply {
            // 设置更新logo
            updateLogoImgRes?.let { ivLogo?.setImageResource(it) }
            // 设置标题字体颜色、大小
            titleTextColor?.let { tvTitle?.setTextColor(it) }
            titleTextSize?.let { tvTitle?.setTextSize(it) }
            // 设置标题字体颜色、大小
            contentTextColor?.let { tvContent?.setTextColor(it) }
            contentTextSize?.let { tvContent?.setTextSize(it) }
            // 更新按钮相关设置
            updateBtnBgColor?.let { sureBtn?.setBackgroundColor(it) }
            updateBtnBgRes?.let { sureBtn?.setBackgroundResource(it) }
            if (sureBtn is TextView) {
                updateBtnTextColor?.let { (sureBtn as? TextView)?.setTextColor(it) }
                updateBtnTextSize?.let { (sureBtn as? TextView)?.setTextSize(it) }
                (sureBtn as? TextView)?.text = updateBtnText
            }

            // 取消按钮相关设置
            cancelBtnBgColor?.let { cancelBtn?.setBackgroundColor(it) }
            cancelBtnBgRes?.let { cancelBtn?.setBackgroundResource(it) }
            if (cancelBtn is TextView) {
                cancelBtnTextColor?.let { (cancelBtn as? TextView)?.setTextColor(it) }
                cancelBtnTextSize?.let { (cancelBtn as? TextView)?.setTextSize(it) }
                (cancelBtn as? TextView)?.text = cancelBtnText
            }
        }
    }

    override fun onBackPressed() {
        // do noting 禁用返回键
    }

    /**
     * 预备下载 进行 6.0权限检查
     */
    private fun preDownLoad() {
        // 6.0 以下不用动态权限申请
        (Build.VERSION.SDK_INT < Build.VERSION_CODES.M ).yes {
            download()
        }.no {
            val writePermission = ContextCompat.checkSelfPermission(this, permission)
            (writePermission == PackageManager.PERMISSION_GRANTED).yes {
                download()
            }.no {
                // 申请权限
                ActivityCompat.requestPermissions(this, arrayOf(permission), PERMISSION_CODE)
            }
        }
    }

    /**
     * 下载判断
     */
    private fun download() {
        // 动态注册广播,8.0 静态注册收不到
        // 开启服务注册,避免直接在Activity中注册广播生命周期随Activity终止而终止
        startService(Intent(this, UpdateAppService::class.java))

        when (updateConfig.downloadBy) {
            // App下载
            DownLoadBy.APP -> {
                (updateConfig.checkWifi && !isWifiConnected()).yes {
                    // 需要进行WiFi判断
                    AlertDialogUtil.show(this, getString(R.string.check_wifi_notice), onSureClick = {
                        realDownload()
                    })
                }.no {
                    // 不需要wifi判断,直接下载
                    realDownload()
                }
            }

            // 浏览器下载
            DownLoadBy.BROWSER -> {
                DownloadAppUtils.downloadForWebView(updateInfo.apkUrl)
            }
        }
    }

    /**
     * 实际下载
     */
    @SuppressLint("SetTextI18n")
    private fun realDownload() {

        if ((updateConfig.force || updateConfig.alwaysShowDownLoadDialog) && sureBtn is TextView) {
            DownloadAppUtils.onError = {
                (sureBtn as? TextView)?.text = uiConfig.downloadFailText
                (updateConfig.alwaysShowDownLoadDialog).yes {
                    hideShowCancelBtn(true)
                }
            }

            DownloadAppUtils.onReDownload = {
                (sureBtn as? TextView)?.text = uiConfig.updateBtnText
            }

            DownloadAppUtils.onProgress = {
                (it == 100).yes {
                    (sureBtn as? TextView)?.text = getString(R.string.install)
                    (updateConfig.alwaysShowDownLoadDialog).yes {
                        hideShowCancelBtn(true)
                    }
                }.no {
                    (sureBtn as? TextView)?.text = "${uiConfig.downloadingBtnText}$it%"
                    (updateConfig.alwaysShowDownLoadDialog).yes {
                        hideShowCancelBtn(false)
                    }
                }
            }
        }

        DownloadAppUtils.download()

        (updateConfig.showDownloadingToast).yes {
            Toast.makeText(this, uiConfig.downloadingToastText, Toast.LENGTH_SHORT).show()
        }

        // 非强制安装且alwaysShowDownLoadDialog为false时,开始下载后取消弹窗
        (!updateConfig.force && !updateConfig.alwaysShowDownLoadDialog).yes {
            finish()
        }
    }

    /**
     * 权限请求结果
     */
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)

        when (requestCode) {
            PERMISSION_CODE -> (grantResults.getOrNull(0) == PackageManager.PERMISSION_GRANTED).yes {
                download()
            }.no {
                ActivityCompat.shouldShowRequestPermissionRationale(this, permission).no {
                    // 显示无权限弹窗
                    AlertDialogUtil.show(this, getString(R.string.no_storage_permission), onSureClick = {
                        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
                        intent.data = Uri.parse("package:$packageName") // 根据包名打开对应的设置界面
                        startActivity(intent)
                    })
                }
            }
        }
    }

    override fun finish() {
        super.finish()
        overridePendingTransition(0, 0)
    }

    companion object {

        fun launch() = globalContext()?.let {
            val intent = Intent(it, UpdateAppActivity::class.java)
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
            it.startActivity(intent)
        }

        private const val permission = Manifest.permission.WRITE_EXTERNAL_STORAGE

        private const val PERMISSION_CODE = 1001
    }
}


================================================
FILE: updateapputils/src/main/java/update/DownloadAppUtils.kt
================================================
package update

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import com.liulishuo.filedownloader.BaseDownloadTask
import com.liulishuo.filedownloader.FileDownloadLargeFileListener
import com.liulishuo.filedownloader.FileDownloader
import extension.*
import util.FileDownloadUtil
import util.SPUtil
import util.SignMd5Util
import java.io.File

/**
 * Created by Teprinciple on 2016/12/13.
 */
internal object DownloadAppUtils {

    const val KEY_OF_SP_APK_PATH = "KEY_OF_SP_APK_PATH"

    /**
     * apk 下载后本地文件路径
     */
    var downloadUpdateApkFilePath: String = ""

    /**
     * 更新信息
     */
    private val updateInfo by lazy { UpdateAppUtils.updateInfo }

    /**
     * context
     */
    private val context by lazy { globalContext()!! }

    /**
     * 是否在下载中
     */
    var isDownloading = false

    /**
     *下载进度回调
     */
    var onProgress: (Int) -> Unit = {}

    /**
     * 下载出错回调
     */
    var onError: () -> Unit = {}

    /**
     * 出错,点击重试回调
     */
    var onReDownload: () -> Unit = {}

    /**
     * 通过浏览器下载APK包
     */
    fun downloadForWebView(url: String) {
        val uri = Uri.parse(url)
        val intent = Intent(Intent.ACTION_VIEW, uri)
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        context.startActivity(intent)
    }

    /**
     * 出错后,点击重试
     */
    fun reDownload() {
        onReDownload.invoke()
        download()
    }

    /**
     * App下载APK包,下载完成后安装
     */
    fun download() {

        (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED).no {
            log("没有SD卡")
            onError.invoke()
            return
        }

        var filePath = ""
        (updateInfo.config.apkSavePath.isNotEmpty()).yes {
            filePath = updateInfo.config.apkSavePath
        }.no {
            // 适配Android10
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !Environment.isExternalStorageLegacy()){
                filePath = (context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath ?: "") + "/apk"
            }else{
                val packageName = context.packageName
                filePath = Environment.getExternalStorageDirectory().absolutePath + "/" + packageName
            }
        }

        // apk 保存名称
        val apkName = if (updateInfo.config.apkSaveName.isNotEmpty()) {
            updateInfo.config.apkSaveName
        } else {
            context.appName
        }

        val apkLocalPath = "$filePath/$apkName.apk"

        downloadUpdateApkFilePath = apkLocalPath

        SPUtil.putBase(KEY_OF_SP_APK_PATH, downloadUpdateApkFilePath)

        FileDownloader.setup(context)

        val downloadTask = FileDownloader.getImpl().create(updateInfo.apkUrl)
            .setPath(apkLocalPath)

        downloadTask
            .addHeader("Accept-Encoding","identity")
            .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")
            .setListener(object : FileDownloadLargeFileListener() {

                override fun pending(task: BaseDownloadTask, soFarBytes: Long, totalBytes: Long) {
                    log("----使用FileDownloader下载-------")
                    log("pending:soFarBytes($soFarBytes),totalBytes($totalBytes)")
                    downloadStart()
                    if(totalBytes < 0){
                        downloadTask.pause()
                    }
                }

                override fun progress(task: BaseDownloadTask, soFarBytes: Long, totalBytes: Long) {
                    downloading(soFarBytes, totalBytes)
                    if(totalBytes < 0){
                        downloadTask.pause()
                    }
                }

                override fun paused(task: BaseDownloadTask, soFarBytes: Long, totalBytes: Long) {
                    log("获取文件总长度失败出错,尝试HTTPURLConnection下载")
                    downloadUpdateApkFilePath.deleteFile()
                    "$downloadUpdateApkFilePath.temp".deleteFile()
                    downloadByHttpUrlConnection(filePath, apkName)
                }

                override fun completed(task: BaseDownloadTask) {
                    downloadComplete()
                }

                override fun error(task: BaseDownloadTask, e: Throwable) {
                    // FileDownloader 下载失败后,再调用 FileDownloadUtil 下载一次
                    // FileDownloader 对码云或者阿里云上的apk文件会下载失败
                    // downloadError(e)
                    log("下载出错,尝试HTTPURLConnection下载")
                    downloadUpdateApkFilePath.deleteFile()
                    "$downloadUpdateApkFilePath.temp".deleteFile()
                    downloadByHttpUrlConnection(filePath, apkName)
                }

                override fun warn(task: BaseDownloadTask) {
                }
            }).start()
    }

    /**
     * 使用 HttpUrlConnection 下载
     */
    private fun downloadByHttpUrlConnection(filePath: String, apkName: String?) {
        FileDownloadUtil.download(
            updateInfo.apkUrl,
            filePath,
            "$apkName.apk",
            onStart = { downloadStart() },
            onProgress = { current, total -> downloading(current, total) },
            onComplete = { downloadComplete() },
            onError = { downloadError(it) }
        )
    }

    /**
     * 开始下载逻辑
     */
    private fun downloadStart() {
        isDownloading = true
        UpdateAppUtils.downloadListener?.onStart()
        UpdateAppReceiver.send(context, 0)
    }

    /**
     * 下载中逻辑
     */
    private fun downloading(soFarBytes: Long, totalBytes: Long) {
//        log("soFarBytes:$soFarBytes--totalBytes:$totalBytes")
        isDownloading = true
        var progress = (soFarBytes * 100.0 / totalBytes).toInt()
        if (progress < 0) progress = 0
        log("progress:$progress")
        UpdateAppReceiver.send(context, progress)
        this@DownloadAppUtils.onProgress.invoke(progress)
        UpdateAppUtils.downloadListener?.onDownload(progress)
    }

    /**
     * 下载完成处理逻辑
     */
    private fun downloadComplete() {
        isDownloading = false
        log("completed")
        this@DownloadAppUtils.onProgress.invoke(100)
        UpdateAppUtils.downloadListener?.onFinish()
        // 校验md5
        (updateInfo.config.needCheckMd5).yes {
            checkMd5(context)
        }.no {
            UpdateAppReceiver.send(context, 100)
        }
    }

    /**
     * 下载失败处理逻辑
     */
    private fun downloadError(e: Throwable) {
        isDownloading = false
        log("error:${e.message}")
        downloadUpdateApkFilePath.deleteFile()
        this@DownloadAppUtils.onError.invoke()
        UpdateAppUtils.downloadListener?.onError(e)
        UpdateAppReceiver.send(context, -1000)
    }

    /**
     * 校验Md5
     *  先获取本应用的MD5值,获取未安装应用的MD5.进行对比
     */
    private fun checkMd5(context: Context) {
        // 当前应用md5
        val localMd5 = SignMd5Util.getAppSignatureMD5()

        // 下载的apk 签名md5
        val apkMd5 = SignMd5Util.getSignMD5FromApk(File(downloadUpdateApkFilePath))
        log("当前应用签名md5:$localMd5")
        log("下载apk签名md5:$apkMd5")

        // 校验结果回调
        UpdateAppUtils.md5CheckResultListener?.onResult(localMd5.equals(apkMd5, true))

        (localMd5.equals(apkMd5, true)).yes {
            log("md5校验成功")
            UpdateAppReceiver.send(context, 100)
        }.no {
            log("md5校验失败")
        }
    }
}

================================================
FILE: updateapputils/src/main/java/update/UpdateAppReceiver.kt
================================================
package update

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.os.Build
import extension.installApk
import extension.no
import extension.yes

/**
 * desc: UpdateAppReceiver
 * author: teprinciple on 2019/06/3.
 */
internal class UpdateAppReceiver : BroadcastReceiver() {

    private val notificationChannel = "1001"

    private val updateConfig by lazy { UpdateAppUtils.updateInfo.config }

    private val uiConfig by lazy { UpdateAppUtils.updateInfo.uiConfig }

    private var lastProgress = 0

    override fun onReceive(context: Context, intent: Intent) {

        when (intent.action) {

            // 下载中
            context.packageName + ACTION_UPDATE -> {
                // 进度
                val progress = intent.getIntExtra(KEY_OF_INTENT_PROGRESS, 0)

                val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

                (progress != -1000).yes {
                    lastProgress = progress
                }

                // 显示通知栏
                val notifyId = 1
                updateConfig.isShowNotification.yes {
                    showNotification(context, notifyId, progress, notificationChannel, nm)
                }

                // 下载完成
                if (progress == 100) {
                    handleDownloadComplete(context, notifyId, nm)
                }
            }

            // 重新下载
            context.packageName + ACTION_RE_DOWNLOAD -> {
                DownloadAppUtils.reDownload()
            }
        }
    }

    /**
     * 下载完成后的逻辑
     */
    private fun handleDownloadComplete(context: Context, notifyId: Int, nm: NotificationManager?) {
        // 关闭通知栏
        nm?.let {
            nm.cancel(notifyId)
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
                nm.deleteNotificationChannel(notificationChannel)
            }
        }

        // 安装apk
        context.installApk(DownloadAppUtils.downloadUpdateApkFilePath)
    }

    /**
     * 通知栏显示
     */
    private fun showNotification(context: Context, notifyId: Int, progress: Int, notificationChannel: String, nm: NotificationManager) {

        val notificationName = "notification"

        // 适配 8.0
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // 通知渠道
            val channel = NotificationChannel(notificationChannel, notificationName, NotificationManager.IMPORTANCE_HIGH)
            channel.enableLights(false)
            // 是否在桌面icon右上角展示小红点
            channel.setShowBadge(false)
            // 是否在久按桌面图标时显示此渠道的通知
            channel.enableVibration(false)
            // 最后在notificationmanager中创建该通知渠道
            nm.createNotificationChannel(channel)
        }

        val builder = Notification.Builder(context)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            builder.setChannelId(notificationChannel)
        }


        // 设置通知图标
        (updateConfig.notifyImgRes > 0).yes {
            builder.setSmallIcon(updateConfig.notifyImgRes)
            builder.setLargeIcon(BitmapFactory.decodeResource(context.resources, updateConfig.notifyImgRes))
        }.no {
            builder.setSmallIcon(android.R.mipmap.sym_def_app_icon)
        }

        // 设置进度
        builder.setProgress(100, lastProgress, false)

        if (progress == -1000) {
            val intent = Intent(context.packageName + ACTION_RE_DOWNLOAD)
            intent.setPackage(context.packageName)
            val pendingIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_CANCEL_CURRENT)
            builder.setContentIntent(pendingIntent)
            // 通知栏标题
            builder.setContentTitle(uiConfig.downloadFailText)
        } else {
            // 通知栏标题
            builder.setContentTitle("${uiConfig.downloadingBtnText}$progress%")
        }


        // 设置只响一次
        builder.setOnlyAlertOnce(true)
        val notification = builder.build()
        nm.notify(notifyId, notification)
    }

    companion object {
        /**
         * 进度key
         */
        private const val KEY_OF_INTENT_PROGRESS = "KEY_OF_INTENT_PROGRESS"

        /**
         * ACTION_UPDATE
         */
        const val ACTION_UPDATE = "teprinciple.update"

        /**
         * ACTION_RE_DOWNLOAD
         */
        const val ACTION_RE_DOWNLOAD = "action_re_download"


        const val REQUEST_CODE = 1001


        /**
         * 发送进度通知
         */
        fun send(context: Context, progress: Int) {
            val intent = Intent(context.packageName + ACTION_UPDATE)
            intent.putExtra(KEY_OF_INTENT_PROGRESS, progress)
            context.sendBroadcast(intent)
        }
    }
}

================================================
FILE: updateapputils/src/main/java/update/UpdateAppService.kt
================================================
package update

import android.app.Service
import android.content.Intent
import android.content.IntentFilter
import android.os.IBinder

/**
 * desc: UpdateAppService
 * author: teprinciple on 2018/11/3.
 */
internal class UpdateAppService : Service() {

    private val updateAppReceiver = UpdateAppReceiver()

    override fun onCreate() {
        super.onCreate()
        // 动态注册receiver 适配8.0 updateAppReceiver 静态注册没收不到广播
        registerReceiver(updateAppReceiver, IntentFilter(packageName + UpdateAppReceiver.ACTION_UPDATE))
        registerReceiver(updateAppReceiver, IntentFilter(packageName + UpdateAppReceiver.ACTION_RE_DOWNLOAD))
    }

    override fun onDestroy() {
        super.onDestroy()
        unregisterReceiver(updateAppReceiver) // 注销广播
    }

    override fun onBind(intent: Intent): IBinder? {
        return null
    }
}


================================================
FILE: updateapputils/src/main/java/update/UpdateAppUtils.kt
================================================
package update

import android.content.Context
import extension.globalContext
import extension.log
import extension.no
import extension.yes
import listener.OnBtnClickListener
import listener.Md5CheckResultListener
import listener.OnInitUiListener
import listener.UpdateDownloadListener
import model.UiConfig
import model.UpdateConfig
import model.UpdateInfo
import ui.UpdateAppActivity
import util.GlobalContextProvider
import util.SPUtil


/**
 * Created by Teprinciple on 2016/11/15.
 */
object UpdateAppUtils {

    // 更新信息对象
    internal val updateInfo by lazy { UpdateInfo() }

    // 下载监听
    internal var downloadListener: UpdateDownloadListener? = null

    // md5校验结果回调
    internal var md5CheckResultListener: Md5CheckResultListener? = null

    // 初始化更新弹窗UI回调
    internal var onInitUiListener: OnInitUiListener? = null

    // "暂不更新"按钮点击事件
    internal var onCancelBtnClickListener: OnBtnClickListener? = null

    // "立即更新"按钮点击事件
    internal var onUpdateBtnClickListener: OnBtnClickListener? = null

    /**
     * 设置apk下载地址
     */
    fun apkUrl(apkUrl: String): UpdateAppUtils {
        updateInfo.apkUrl = apkUrl
        return this
    }

    /**
     * 设置更新标题
     */
    fun updateTitle(title: CharSequence): UpdateAppUtils {
        updateInfo.updateTitle = title
        return this
    }

    /**
     * 设置更新内容
     */
    fun updateContent(content: CharSequence): UpdateAppUtils {
        updateInfo.updateContent = content
        return this
    }

    /**
     * 设置更新配置
     */
    fun updateConfig(config: UpdateConfig): UpdateAppUtils {
        updateInfo.config = config
        return this
    }

    /**
     * 设置UI配置
     */
    fun uiConfig(uiConfig: UiConfig): UpdateAppUtils {
        updateInfo.uiConfig = uiConfig
        return this
    }

    /**
     * 设置下载监听
     */
    fun setUpdateDownloadListener(listener: UpdateDownloadListener?): UpdateAppUtils {
        this.downloadListener = listener
        return this
    }

    /**
     * 设置md5校验结果监听
     */
    fun setMd5CheckResultListener(listener: Md5CheckResultListener?): UpdateAppUtils {
        this.md5CheckResultListener = listener
        return this
    }

    /**
     * 设置初始化UI监听
     */
    fun setOnInitUiListener(listener: OnInitUiListener?): UpdateAppUtils {
        this.onInitUiListener = listener
        return this
    }

    /**
     * 设置 “暂不更新” 按钮点击事件
     */
    fun setCancelBtnClickListener(listener: OnBtnClickListener?): UpdateAppUtils {
        this.onCancelBtnClickListener = listener
        return this
    }

    /**
     * 设置 “立即更新” 按钮点击事件
     */
    fun setUpdateBtnClickListener(listener: OnBtnClickListener?): UpdateAppUtils {
        this.onUpdateBtnClickListener = listener
        return this
    }

    /**
     * 检查更新
     */
    fun update() {

        if(globalContext() == null){
            log("请先调用初始化init")
            return
        }

        val keyName = (globalContext()?.packageName ?: "") + updateInfo.config.serverVersionName
        // 设置每次显示,设置本次显示及强制更新 每次都显示弹窗
        (updateInfo.config.alwaysShow || updateInfo.config.thisTimeShow || updateInfo.config.force).yes {
            UpdateAppActivity.launch()
        }.no {
            val hasShow = SPUtil.getBoolean(keyName, false)
            (hasShow).no { UpdateAppActivity.launch() }
        }
        SPUtil.putBase(keyName, true)
    }

    /* 未缓存apk
    /**
     * 删除已安装 apk
     */
    fun deleteInstalledApk() {
        val apkPath = SPUtil.getString(DownloadAppUtils.KEY_OF_SP_APK_PATH, "")
        val appVersionCode = Utils.getAPPVersionCode()
        val apkVersionCode = Utils.getApkVersionCode(apkPath)
        log("appVersionCode:$appVersionCode")
        log("apkVersionCode:$apkVersionCode")
        (apkPath.isNotEmpty() && appVersionCode == apkVersionCode && apkVersionCode > 0).yes {
            Utils.deleteFile(apkPath)
        }
    }
     */

    /**
     * 获取单例对象
     */
    @JvmStatic
    fun getInstance() = this

    /**
     * 初始化,非必须。解决部分手机 通过UpdateFileProvider 获取不到context情况使用
     * * @param context 提供全局context。
     */
    @JvmStatic
    fun init(context: Context){
        GlobalContextProvider.mContext = context.applicationContext
        log("外部初始化context")
    }
}

================================================
FILE: updateapputils/src/main/java/update/UpdateFileProvider.kt
================================================
package update

import android.support.v4.content.FileProvider
import extension.log
import extension.yes
import util.GlobalContextProvider

/**
 * desc: UpdateFileProvider
 * time: 2019/7/10
 * @author Teprinciple
 */
class UpdateFileProvider : FileProvider() {
    override fun onCreate(): Boolean {
        val result = super.onCreate()
        (GlobalContextProvider.mContext == null && context != null).yes {
            GlobalContextProvider.mContext = context
            log("内部Provider初始化context:" + GlobalContextProvider.mContext)
        }
        return result
    }
}

================================================
FILE: updateapputils/src/main/java/util/AlertDialogUtil.kt
================================================
package util

import android.app.Activity
import android.app.AlertDialog
import com.teprinciple.updateapputils.R
import extension.string

/**
 * desc: AlertDialogUtil
 * time: 2018/8/20
 * @author teprinciple
 */
internal object AlertDialogUtil {

    fun show(
        activity: Activity,
        message: String,
        onCancelClick: () -> Unit = {},
        onSureClick: () -> Unit = {},
        cancelable: Boolean = false,
        title: String = string(R.string.notice),
        cancelText: String = string(R.string.cancel),
        sureText: String = string(R.string.sure)
    ) {
        AlertDialog.Builder(activity, R.style.AlertDialog)
            .setTitle(title)
            .setMessage(message)
            .setPositiveButton(sureText) { _, _ ->
                onSureClick.invoke()
            }
            .setNegativeButton(cancelText) { _, _ ->
                onCancelClick.invoke()
            }
            .setCancelable(cancelable)
            .create()
            .show()
    }
}

================================================
FILE: updateapputils/src/main/java/util/FileDownloadUtil.kt
================================================
package util

import extension.log
import extension.no
import extension.yes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileOutputStream
import java.io.InputStream
import java.io.OutputStream
import java.net.HttpURLConnection
import java.net.HttpURLConnection.HTTP_OK
import java.net.URL

/**
 * desc: 文件下载 当 FileDownloader 对某些apk下载失败时(比如:放在阿里云,码云上apk) 使用该工具类下载
 * time: 2019/8/28
 * @author teprinciple
 */
internal object FileDownloadUtil {

    /**
     * 下载文件
     * @param url 文件地址
     * @param fileSavePath 文件存储地址
     * @param fileName 文件存储名称
     * @param onStart 开始下载回调
     * @param onProgress 下载中回调
     * @param onComplete 下载完成回调
     * @param onError 下载失败回调
     */
    fun download(
        url: String,
        fileSavePath: String,
        fileName: String?,
        onStart: () -> Unit = {},
        onProgress: (current: Long, total: Long) -> Unit = { _, _ -> },
        onComplete: () -> Unit = {},
        onError: (Throwable) -> Unit = {}
    ) {
        GlobalScope.launch(Dispatchers.IO) {
            log("----使用HttpURLConnection下载----")
            onStart.invoke()
            var connection: HttpURLConnection? = null
            var outputStream: FileOutputStream? = null

            kotlin.runCatching {
                connection = URL(url).openConnection() as HttpURLConnection
                outputStream = FileOutputStream(File(fileSavePath, fileName))

                connection?.apply {
                    requestMethod = "GET"
                    setRequestProperty("Charset", "utf-8")
                    setRequestProperty("Accept-Encoding", "identity")
                    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")
                    connect()
                }

                val responseCode = connection!!.responseCode
                if (responseCode == HTTP_OK) {
                    val total = connection!!.contentLength
                    var progress = -1
                    connection!!.inputStream.use { input ->
                        outputStream.use { output ->
                            input.copyToWithProgress(output!!) {
                                val pro = (it * 100.0 / total).toInt()
                                (progress != pro).yes {
                                    GlobalScope.launch(Dispatchers.Main) {
                                        onProgress(it, total.toLong())
                                    }
                                }
                                progress = pro
                            }
                        }
                    }
                }else{
                    throw Throwable(message = "文件下载错误")
                }
            }.onSuccess {
                connection?.disconnect()
                outputStream?.close()
                log("HttpURLConnection下载完成")
                GlobalScope.launch(Dispatchers.Main) {
                    (File(fileSavePath).length() > 0L).yes{
                        onComplete.invoke()
                    }.no {
                        onError.invoke(Throwable(message = "文件下载错误"))
                    }
                }
            }.onFailure {
                connection?.disconnect()
                outputStream?.close()
                log("HttpURLConnection下载失败:${it.message}")
                GlobalScope.launch(Dispatchers.Main) {
                    onError.invoke(it)
                }
            }
        }
    }
}

fun InputStream.copyToWithProgress(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE, currentByte: (Long) -> Unit = {}): Long {
    var bytesCopied: Long = 0
    val buffer = ByteArray(bufferSize)
    var bytes = read(buffer)
    while (bytes >= 0) {
        out.write(buffer, 0, bytes)
        bytesCopied += bytes
        bytes = read(buffer)
        currentByte.invoke(bytesCopied)
    }
    return bytesCopied
}

================================================
FILE: updateapputils/src/main/java/util/GlobalContextProvider.kt
================================================
package util

import android.annotation.SuppressLint
import android.content.Context

/**
 * desc: 提供context.
 */
@SuppressLint("StaticFieldLeak")
internal object GlobalContextProvider {

    /** 全局context 提供扩展globalContext */
    internal var mContext: Context? = null
}

================================================
FILE: updateapputils/src/main/java/util/SPUtil.kt
================================================
package util

import android.app.Activity
import android.content.SharedPreferences
import extension.globalContext

/**
 * SharedPreferences 数据保存
 */
internal object SPUtil {

    fun putBase(keyName: String, value: Any): Boolean? {
        val sharedPreferences = getSp()
        val editor: SharedPreferences.Editor? = sharedPreferences?.edit()
        when (value) {
            is Int -> editor?.putInt(keyName, value)
            is Boolean -> editor?.putBoolean(keyName, value)
            is Float -> editor?.putFloat(keyName, value)
            is String -> editor?.putString(keyName, value)
            is Long -> editor?.putLong(keyName, value)
            else -> throw IllegalArgumentException("SharedPreferences can,t be save this type")
        }
        return editor?.commit()
    }

    fun getBoolean(keyName: String, defaultValue: Boolean = false): Boolean {
        val sharedPreferences = getSp()
        return sharedPreferences?.getBoolean(keyName, defaultValue) ?: false
    }

    fun getString(keyName: String, defaultValue: String? = null): String {
        val sharedPreferences = getSp()
        return sharedPreferences?.getString(keyName, defaultValue) ?: ""
    }

    private fun getSp(): SharedPreferences? {
        if (globalContext() == null) return null
        return globalContext()!!.getSharedPreferences(globalContext()!!.packageName, Activity.MODE_PRIVATE)
    }
}

================================================
FILE: updateapputils/src/main/java/util/SignMd5Util.kt
================================================
package util

import android.content.pm.PackageManager
import android.content.pm.Signature
import extension.globalContext
import java.io.File
import java.io.IOException
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.Certificate
import java.util.*
import java.util.jar.JarEntry
import java.util.jar.JarFile
import kotlin.experimental.and

/**
 * desc: 获取签名 md5
 * time: 2019/6/21
 * @author teprinciple
 */
internal object SignMd5Util {

    private val HEX_DIGITS = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')

    /**
     * 获取当前应用签名文件md5
     */
    fun getAppSignatureMD5(): String {
        val packageName = globalContext()?.packageName ?: ""
        if (packageName.isEmpty()) return ""
        val signature = getAppSignature(packageName)
        return if (signature == null || signature.isEmpty()) {
            ""
        } else {
            bytes2HexString(hashTemplate(signature[0].toByteArray(), "MD5"))
                .replace("(?<=[0-9A-F]{2})[0-9A-F]{2}".toRegex(), ":$0")
        }
    }

    /**
     * 获取未安装apk 签名文件md5
     */
    fun getSignMD5FromApk(file: File): String {
        val signatures = ArrayList<String>()
        val jarFile = JarFile(file)
        try {
            val je = jarFile.getJarEntry("AndroidManifest.xml")
            val readBuffer = ByteArray(8192)
            val certs = loadCertificates(jarFile, je, readBuffer)
            if (certs != null) {
                for (c in certs) {
                    val sig = bytes2HexString(hashTemplate(c.encoded, "MD5"))
                        .replace("(?<=[0-9A-F]{2})[0-9A-F]{2}".toRegex(), ":$0")
                    signatures.add(sig)
                }
            }
        } catch (ex: Exception) {
        }
        return signatures.getOrNull(0) ?: ""
    }

    private fun getAppSignature(packageName: String): Array<Signature>? {
        if (packageName.isEmpty()) return null
        return try {
            val pm = globalContext()?.packageManager
            val pi = pm?.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)
            pi?.signatures
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }

    private fun hashTemplate(data: ByteArray?, algorithm: String): ByteArray? {
        if (data == null || data.isEmpty()) return null
        return try {
            val md = MessageDigest.getInstance(algorithm)
            md.update(data)
            md.digest()
        } catch (e: NoSuchAlgorithmException) {
            e.printStackTrace()
            null
        }
    }

    private fun bytes2HexString(bytes: ByteArray?): String {
        if (bytes == null) return ""
        val len = bytes.size
        if (len <= 0) return ""
        val ret = CharArray(len shl 1)
        var i = 0
        var j = 0
        while (i < len) {
            ret[j++] = HEX_DIGITS[bytes[i].toInt().shr(4) and 0x0f]
            ret[j++] = HEX_DIGITS[(bytes[i] and 0x0f).toInt()]
            i++
        }
        return String(ret)
    }

    /**
     * 加载签名
     */
    private fun loadCertificates(jarFile: JarFile, je: JarEntry?, readBuffer: ByteArray): Array<Certificate>? {
        try {
            val inputStream = jarFile.getInputStream(je)
            while (inputStream.read(readBuffer, 0, readBuffer.size) != -1) {
            }
            inputStream.close()
            return je?.certificates
        } catch (e: IOException) {
        }
        return null
    }
}

================================================
FILE: updateapputils/src/main/res/anim/dialog_enter.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:duration="200"
        android:fromAlpha="0"
        android:toAlpha="1"/>
</set>

================================================
FILE: updateapputils/src/main/res/anim/dialog_out.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:duration="200"
        android:fromAlpha="1"
        android:toAlpha="0"/>
</set>

================================================
FILE: updateapputils/src/main/res/drawable/bg_update_btn.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <corners android:radius="6dp"/>
    <solid android:color="#1296db"/>
</shape>

================================================
FILE: updateapputils/src/main/res/drawable/bg_update_dialog.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="@color/white"/>
    <corners android:radius="6dp"/>
</shape>

================================================
FILE: updateapputils/src/main/res/layout/view_update_dialog_plentiful.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="280dp"
    android:layout_height="wrap_content"
    android:background="@drawable/bg_update_dialog"
    android:paddingBottom="10dp">

    <ImageView
        android:id="@+id/iv_update_logo"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:layout_marginTop="15dp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:srcCompat="@drawable/ic_update_logo"/>

    <TextView
        android:id="@+id/tv_update_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:textColor="@color/text_title"
        android:textSize="16sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/iv_update_logo"
        tools:text="版本更新啦!"/>

    <ScrollView
        android:id="@+id/scrollView2"
        android:layout_width="match_parent"
        android:layout_height="90dp"
        android:layout_marginTop="10dp"
        android:overScrollMode="never"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_update_title">

        <TextView
            android:id="@+id/tv_update_content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="top"
            android:gravity="start"
            android:lineSpacingExtra="5dp"
            android:paddingLeft="20dp"
            android:paddingRight="20dp"
            android:textColor="@color/text_content"
            android:textSize="14sp"
            tools:text="1、快来升级最新版本\n2、这次更漂亮了\n3、快点来吧"/>
    </ScrollView>

    <TextView
        android:id="@+id/btn_update_sure"
        android:layout_width="0dp"
        android:layout_height="35dp"
        android:layout_marginStart="20dp"
        android:layout_marginTop="10dp"
        android:layout_marginEnd="20dp"
        android:background="@drawable/bg_update_btn"
        android:gravity="center"
        android:text="@string/update_now"
        android:textColor="@color/white"
        android:textSize="14sp"
        app:layout_constraintBottom_toTopOf="@+id/btn_update_cancel"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/scrollView2"
        app:layout_goneMarginBottom="10dp"/>

    <TextView
        android:id="@+id/btn_update_cancel"
        android:layout_width="0dp"
        android:layout_height="35dp"
        android:layout_marginStart="20dp"
        android:layout_marginTop="5dp"
        android:layout_marginEnd="20dp"
        android:gravity="center"
        android:text="@string/update_cancel"
        android:textColor="@color/text_content"
        android:textSize="14sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btn_update_sure"
        app:layout_goneMarginTop="10dp"/>

</android.support.constraint.ConstraintLayout>


================================================
FILE: updateapputils/src/main/res/layout/view_update_dialog_simple.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="280dp"
    android:layout_height="wrap_content"
    android:background="@drawable/bg_update_dialog"
    android:orientation="vertical">

    <TextView
        android:id="@+id/tv_update_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="15dp"
        android:layout_marginTop="15dp"
        android:layout_marginRight="15dp"
        android:gravity="center_horizontal"
        android:textColor="@color/text_title"
        android:textSize="18sp"
        tools:text="版本更新啦!"/>

    <ScrollView
        android:layout_marginTop="15dp"
        android:layout_marginBottom="10dp"
        android:layout_width="match_parent"
        android:layout_height="90dp"
        android:overScrollMode="never">

        <TextView
            android:id="@+id/tv_update_content"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="top"
            android:gravity="start"
            android:lineSpacingExtra="5dp"
            android:paddingLeft="12dp"
            android:paddingRight="12dp"
            android:textColor="@color/text_content"
            android:textSize="14sp"/>
    </ScrollView>

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#DFDFDF"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="44dp"
        android:orientation="horizontal">

        <TextView
            android:id="@+id/btn_update_cancel"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:text="@string/update_cancel"
            android:textColor="@color/text_content"
            android:textSize="16sp"/>

        <View
            android:id="@+id/view_line"
            android:layout_width="1dp"
            android:layout_height="match_parent"
            android:background="#DFDFDF"/>

        <TextView
            android:id="@+id/btn_update_sure"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:gravity="center"
            android:text="@string/update_now"
            android:textColor="@color/text_blue"
            android:textSize="16sp"/>
    </LinearLayout>

</LinearLayout>


================================================
FILE: updateapputils/src/main/res/values/colors.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="white">#ffffff</color>
    <color name="text_blue">#0076FF</color>
    <color name="text_title">#333333</color>
    <color name="text_content">#555555</color>
</resources>


================================================
FILE: updateapputils/src/main/res/values/strings.xml
================================================
<resources>
    <string name="notice">提示</string>
    <string name="cancel">取消</string>
    <string name="sure">确认</string>
    <string name="update_now">立即更新</string>
    <string name="update_cancel">暂不更新</string>
    <string name="update_title">版本更新啦!</string>
    <string name="update_content">发现新版本,立即更新</string>
    <string name="download_fail">下载出错,点击重试</string>
    <string name="toast_download_apk">更新下载中...</string>
    <string name="downloading">下载中</string>
    <string name="no_storage_permission">"暂无储存权限,是否前往打开"</string>
    <string name="check_wifi_notice">"当前没有连接Wifi,是否继续下载"</string>
    <string name="install">立即安装</string>
</resources>


================================================
FILE: updateapputils/src/main/res/values/styles.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="CustomDialog" parent="@android:style/Theme.Dialog">
        <item name="android:windowFrame">@null</item>
        <item name="android:windowIsFloating">true</item>
        <item name="android:windowIsTranslucent">false</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowBackground">@drawable/bg_update_dialog</item>
        <item name="android:backgroundDimEnabled">true</item>
    </style>

    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"/>

    <style name="DialogActivityTheme" parent="Theme.AppCompat.Dialog">
        <item name="android:windowFrame">@null</item>
        <!-- 边框 -->
        <item name="android:windowIsFloating">true</item>
        <!-- 是否浮现在activity之上 -->
        <item name="android:windowIsTranslucent">true</item>
        <!-- 半透明 -->
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowBackground">@android:color/transparent</item>
        <item name="android:background">@android:color/transparent</item>
        <item name="android:backgroundDimEnabled">true</item>
        <!-- 模糊 -->
        <item name="android:windowCloseOnTouchOutside">false</item>
        <!-- 重点,去掉标题 -->
        <item name="windowNoTitle">true</item>
    </style>

    <style name="AlertDialog" parent="Base.Theme.AppCompat.Light.Dialog.Alert">
        <!--<item name="android:windowBackground">@android:color/white</item>-->
        <!--<item name="android:windowNoTitle">true</item>-->
    </style>
</resources>

================================================
FILE: updateapputils/src/main/res/values-en/strings.xml
================================================
<resources>
    <string name="notice">Notice</string>
    <string name="cancel">Cancel</string>
    <string name="sure">OK</string>
    <string name="update_now">Update</string>
    <string name="update_cancel">Cancel</string>
    <string name="update_title">New version!</string>
    <string name="update_content">New version get ready,update now</string>
    <string name="download_fail">Download error, Click retry</string>
    <string name="toast_download_apk">Start downloading...</string>
    <string name="downloading">downloading</string>
    <string name="no_storage_permission">Please allow access to storage permissions</string>
    <string name="check_wifi_notice">Current net type is not Wifi, Whether to continue</string>
    <string name="install">Install</string>
</resources>


================================================
FILE: updateapputils/src/main/res/xml/network_security_config.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

================================================
FILE: updateapputils/src/main/res/xml/update_file_paths.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-path path="" name="files_root" />
</paths>
Download .txt
gitextract_os787tpu/

├── .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
│               ├── layout/
│               │   ├── activity_java_demo.xml
│               │   ├── activity_main.xml
│               │   ├── check_md5_demo_activity.xml
│               │   └── view_update_dialog_custom.xml
│               ├── values/
│               │   ├── colors.xml
│               │   ├── dimens.xml
│               │   ├── strings.xml
│               │   └── styles.xml
│               └── values-w820dp/
│                   └── dimens.xml
├── build.gradle
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── readme/
│   ├── README_1.5.2.md
│   ├── version.md
│   └── 自定义UI.md
├── settings.gradle
├── 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
                ├── layout/
                │   ├── view_update_dialog_plentiful.xml
                │   └── view_update_dialog_simple.xml
                ├── values/
                │   ├── colors.xml
                │   ├── strings.xml
                │   └── styles.xml
                ├── values-en/
                │   └── strings.xml
                └── xml/
                    ├── network_security_config.xml
                    └── update_file_paths.xml
Download .txt
SYMBOL INDEX (100 symbols across 4 files)

FILE: app/src/androidTest/java/com/example/teprinciple/updateappdemo/ExampleInstrumentedTest.java
  class ExampleInstrumentedTest (line 17) | @RunWith(AndroidJUnit4.class)
    method useAppContext (line 19) | @Test

FILE: app/src/main/java/com/example/teprinciple/updateappdemo/JavaDemoActivity.java
  class JavaDemoActivity (line 22) | public class JavaDemoActivity extends AppCompatActivity {
    method onCreate (line 28) | @Override

FILE: app/src/main/java/com/example/teprinciple/updateappdemo/SpanUtils.java
  class SpanUtils (line 69) | public final class SpanUtils {
    method SpanUtils (line 142) | public SpanUtils(Context context) {
    method setDefault (line 149) | private void setDefault() {
    method setFlag (line 198) | public SpanUtils setFlag(final int flag) {
    method setForegroundColor (line 210) | public SpanUtils setForegroundColor(@ColorInt final int color) {
    method setBackgroundColor (line 222) | public SpanUtils setBackgroundColor(@ColorInt final int color) {
    method setLineHeight (line 234) | public SpanUtils setLineHeight(@IntRange(from = 0) final int lineHeigh...
    method setLineHeight (line 252) | public SpanUtils setLineHeight(@IntRange(from = 0) final int lineHeight,
    method setQuoteColor (line 266) | public SpanUtils setQuoteColor(@ColorInt final int color) {
    method setQuoteColor (line 281) | public SpanUtils setQuoteColor(@ColorInt final int color,
    method setLeadingMargin (line 299) | public SpanUtils setLeadingMargin(@IntRange(from = 0) final int first,
    method setBullet (line 313) | public SpanUtils setBullet(@IntRange(from = 0) final int gapWidth) {
    method setBullet (line 328) | public SpanUtils setBullet(@ColorInt final int color,
    method setFontSize (line 344) | public SpanUtils setFontSize(@IntRange(from = 0) final int size) {
    method setFontSize (line 357) | public SpanUtils setFontSize(@IntRange(from = 0) final int size, final...
    method setFontProportion (line 370) | public SpanUtils setFontProportion(final float proportion) {
    method setFontXProportion (line 382) | public SpanUtils setFontXProportion(final float proportion) {
    method setStrikethrough (line 392) | public SpanUtils setStrikethrough() {
    method setUnderline (line 402) | public SpanUtils setUnderline() {
    method setSuperscript (line 412) | public SpanUtils setSuperscript() {
    method setSubscript (line 422) | public SpanUtils setSubscript() {
    method setBold (line 432) | public SpanUtils setBold() {
    method setNotBold (line 442) | public SpanUtils setNotBold() {
    method setItalic (line 452) | public SpanUtils setItalic() {
    method setBoldItalic (line 462) | public SpanUtils setBoldItalic() {
    method setFontFamily (line 479) | public SpanUtils setFontFamily(@NonNull final String fontFamily) {
    method setTypeface (line 491) | public SpanUtils setTypeface(@NonNull final Typeface typeface) {
    method setAlign (line 508) | public SpanUtils setAlign(@NonNull final Alignment alignment) {
    method setClickSpan (line 521) | public SpanUtils setClickSpan(@NonNull final ClickableSpan clickSpan) {
    method setUrl (line 534) | public SpanUtils setUrl(@NonNull final String url) {
    method setBlur (line 554) | public SpanUtils setBlur(@FloatRange(from = 0, fromInclusive = false) ...
    method setShader (line 568) | public SpanUtils setShader(@NonNull final Shader shader) {
    method setShadow (line 586) | public SpanUtils setShadow(@FloatRange(from = 0, fromInclusive = false...
    method setSpans (line 605) | public SpanUtils setSpans(@NonNull final Object... spans) {
    method append (line 619) | public SpanUtils append(@NonNull final CharSequence text) {
    method appendLine (line 630) | public SpanUtils appendLine() {
    method appendLine (line 641) | public SpanUtils appendLine(@NonNull final CharSequence text) {
    method appendImage (line 654) | public SpanUtils appendImage(@NonNull final Bitmap bitmap) {
    method appendImage (line 673) | public SpanUtils appendImage(@NonNull final Bitmap bitmap, @Align fina...
    method appendImage (line 687) | public SpanUtils appendImage(@NonNull final Drawable drawable) {
    method appendImage (line 706) | public SpanUtils appendImage(@NonNull final Drawable drawable, @Align ...
    method appendImage (line 720) | public SpanUtils appendImage(@NonNull final Uri uri) {
    method appendImage (line 739) | public SpanUtils appendImage(@NonNull final Uri uri, @Align final int ...
    method appendImage (line 753) | public SpanUtils appendImage(@DrawableRes final int resourceId) {
    method appendImage (line 772) | public SpanUtils appendImage(@DrawableRes final int resourceId, @Align...
    method appendSpace (line 787) | public SpanUtils appendSpace(@IntRange(from = 0) final int size) {
    method appendSpace (line 800) | public SpanUtils appendSpace(@IntRange(from = 0) final int size, @Colo...
    method apply (line 807) | private void apply(final int type) {
    method create (line 817) | public SpannableStringBuilder create() {
    method applyLast (line 822) | private void applyLast() {
    method updateCharCharSequence (line 833) | private void updateCharCharSequence() {
    method updateImage (line 968) | private void updateImage() {
    method updateSpace (line 983) | private void updateSpace() {
    class CustomLineHeightSpan (line 990) | class CustomLineHeightSpan extends CharacterStyle
      method CustomLineHeightSpan (line 1001) | CustomLineHeightSpan(int height, int verticalAlignment) {
      method chooseHeight (line 1006) | @Override
      method updateDrawState (line 1033) | @Override
    class SpaceSpan (line 1039) | class SpaceSpan extends ReplacementSpan {
      method SpaceSpan (line 1044) | private SpaceSpan(final int width) {
      method SpaceSpan (line 1048) | private SpaceSpan(final int width, final int color) {
      method getSize (line 1054) | @Override
      method draw (line 1062) | @Override
    class CustomQuoteSpan (line 1081) | class CustomQuoteSpan implements LeadingMarginSpan {
      method CustomQuoteSpan (line 1087) | private CustomQuoteSpan(final int color, final int stripeWidth, fina...
      method getLeadingMargin (line 1094) | public int getLeadingMargin(final boolean first) {
      method drawLeadingMargin (line 1098) | public void drawLeadingMargin(final Canvas c, final Paint p, final i...
    class CustomBulletSpan (line 1115) | class CustomBulletSpan implements LeadingMarginSpan {
      method CustomBulletSpan (line 1123) | private CustomBulletSpan(final int color, final int radius, final in...
      method getLeadingMargin (line 1129) | public int getLeadingMargin(final boolean first) {
      method drawLeadingMargin (line 1133) | public void drawLeadingMargin(final Canvas c, final Paint p, final i...
    class CustomTypefaceSpan (line 1162) | @SuppressLint("ParcelCreator")
      method CustomTypefaceSpan (line 1167) | private CustomTypefaceSpan(final Typeface type) {
      method updateDrawState (line 1172) | @Override
      method updateMeasureState (line 1177) | @Override
      method apply (line 1182) | private void apply(final Paint paint, final Typeface tf) {
    class CustomImageSpan (line 1206) | class CustomImageSpan extends CustomDynamicDrawableSpan {
      method CustomImageSpan (line 1211) | private CustomImageSpan(final Bitmap b, final int verticalAlignment) {
      method CustomImageSpan (line 1219) | private CustomImageSpan(final Drawable d, final int verticalAlignmen...
      method CustomImageSpan (line 1227) | private CustomImageSpan(final Uri uri, final int verticalAlignment) {
      method CustomImageSpan (line 1232) | private CustomImageSpan(@DrawableRes final int resourceId, final int...
      method getDrawable (line 1237) | @Override
    class CustomDynamicDrawableSpan (line 1272) | abstract class CustomDynamicDrawableSpan extends ReplacementSpan {
      method CustomDynamicDrawableSpan (line 1284) | private CustomDynamicDrawableSpan() {
      method CustomDynamicDrawableSpan (line 1288) | private CustomDynamicDrawableSpan(final int verticalAlignment) {
      method getDrawable (line 1292) | public abstract Drawable getDrawable();
      method getSize (line 1294) | @Override
      method draw (line 1324) | @Override
      method getCachedDrawable (line 1353) | private Drawable getCachedDrawable() {
    class ShaderSpan (line 1369) | class ShaderSpan extends CharacterStyle implements UpdateAppearance {
      method ShaderSpan (line 1372) | private ShaderSpan(final Shader shader) {
      method updateDrawState (line 1376) | @Override
    class ShadowSpan (line 1382) | class ShadowSpan extends CharacterStyle implements UpdateAppearance {
      method ShadowSpan (line 1387) | private ShadowSpan(final float radius,
      method updateDrawState (line 1397) | @Override

FILE: updateapputils/src/androidTest/java/teprinciple/library/ExampleInstrumentedTest.java
  class ExampleInstrumentedTest (line 17) | @RunWith(AndroidJUnit4.class)
    method useAppContext (line 19) | @Test
Condensed preview — 75 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (172K chars).
[
  {
    "path": ".github/workflows/android.yml",
    "chars": 1015,
    "preview": "# workflow的名称,会显示在github 的项目的Actions的右边列表中,如下图\nname: Android CI\n\n# 在满足以下条件触发这个workflow\non:\n  push:\n    # 在指定的远程分支 master"
  },
  {
    "path": ".gitignore",
    "chars": 118,
    "preview": "*.iml\n.gradle\n/local.properties\n/.idea/workspace.xml\n/.idea/libraries\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n"
  },
  {
    "path": ".idea/vcs.xml",
    "chars": 180,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping dire"
  },
  {
    "path": "README.md",
    "chars": 7701,
    "preview": "# UpdateAppUtils2.0\n\n [ ![](https://img.shields.io/badge/platform-android-green.svg) ](http://developer.android.com/inde"
  },
  {
    "path": "app/.gitignore",
    "chars": 7,
    "preview": "/build\n"
  },
  {
    "path": "app/build.gradle",
    "chars": 1338,
    "preview": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-android-extensions'\n\nandroi"
  },
  {
    "path": "app/proguard-rules.pro",
    "chars": 677,
    "preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in C:"
  },
  {
    "path": "app/src/androidTest/java/com/example/teprinciple/updateappdemo/ExampleInstrumentedTest.java",
    "chars": 778,
    "preview": "package com.example.teprinciple.updateappdemo;\n\nimport android.content.Context;\nimport android.support.test.Instrumentat"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 887,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:to"
  },
  {
    "path": "app/src/main/java/com/example/teprinciple/updateappdemo/CheckMd5DemoActivity.kt",
    "chars": 2460,
    "preview": "package com.example.teprinciple.updateappdemo\n\nimport android.os.Bundle\nimport android.os.Environment\nimport android.sup"
  },
  {
    "path": "app/src/main/java/com/example/teprinciple/updateappdemo/JavaDemoActivity.java",
    "chars": 2711,
    "preview": "package com.example.teprinciple.updateappdemo;\n\nimport android.os.Bundle;\nimport android.support.annotation.Nullable;\nim"
  },
  {
    "path": "app/src/main/java/com/example/teprinciple/updateappdemo/MainActivity.kt",
    "chars": 6184,
    "preview": "package com.example.teprinciple.updateappdemo\n\nimport android.content.Intent\nimport android.graphics.Color\nimport androi"
  },
  {
    "path": "app/src/main/java/com/example/teprinciple/updateappdemo/SpanUtils.java",
    "chars": 43248,
    "preview": "package com.example.teprinciple.updateappdemo;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\n"
  },
  {
    "path": "app/src/main/res/drawable/bg_btn.xml",
    "chars": 187,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <corners a"
  },
  {
    "path": "app/src/main/res/layout/activity_java_demo.xml",
    "chars": 526,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  "
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "chars": 1941,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    "
  },
  {
    "path": "app/src/main/res/layout/check_md5_demo_activity.xml",
    "chars": 949,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    "
  },
  {
    "path": "app/src/main/res/layout/view_update_dialog_custom.xml",
    "chars": 3600,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    "
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "chars": 297,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#3F51B5</color>\n    <color name=\"color"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "chars": 211,
    "preview": "<resources>\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "chars": 69,
    "preview": "<resources>\n    <string name=\"app_name\">Update</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "chars": 835,
    "preview": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">"
  },
  {
    "path": "app/src/main/res/values-w820dp/dimens.xml",
    "chars": 372,
    "preview": "<resources>\n    <!-- Example customization of dimensions originally defined in res/values/dimens.update_file_paths\n     "
  },
  {
    "path": "build.gradle",
    "chars": 1156,
    "preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    e"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 232,
    "preview": "#Mon Jun 03 12:27:34 CST 2019\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_"
  },
  {
    "path": "gradle.properties",
    "chars": 731,
    "preview": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will o"
  },
  {
    "path": "gradlew",
    "chars": 4971,
    "preview": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start "
  },
  {
    "path": "gradlew.bat",
    "chars": 2314,
    "preview": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem "
  },
  {
    "path": "readme/README_1.5.2.md",
    "chars": 2364,
    "preview": "# updateapputils\n\n### 一行代码,快速实现app在线下载更新  A simple library for Android update app\n### 适配Android6.0、7.0、8.0\n![](update.gi"
  },
  {
    "path": "readme/version.md",
    "chars": 999,
    "preview": "### 更新日志\n#### 2.3.0\n* 修复部分手机context空指针异常\n#### 2.2.1\n* 优化代码\n* 修复部分bug\n#### 2.2.0\n* 适配Android 10\n* 修复部分bug\n#### 2.1.0\n* 增加"
  },
  {
    "path": "readme/自定义UI.md",
    "chars": 1519,
    "preview": "## 完全自定义UI\n\n### 1、创建你的layout(必须)\n你可以创建任意你想要的UI布局([参考 view_update_dialog_custom.xml](https://github.com/teprinciple/Updat"
  },
  {
    "path": "settings.gradle",
    "chars": 34,
    "preview": "include ':app', ':updateapputils'\n"
  },
  {
    "path": "updateapputils/.gitignore",
    "chars": 7,
    "preview": "/build\n"
  },
  {
    "path": "updateapputils/build.gradle",
    "chars": 1292,
    "preview": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android-extensions'\napply plugin: 'kotlin-android'\napply plugi"
  },
  {
    "path": "updateapputils/proguard-rules.pro",
    "chars": 940,
    "preview": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /U"
  },
  {
    "path": "updateapputils/src/androidTest/java/teprinciple/library/ExampleInstrumentedTest.java",
    "chars": 747,
    "preview": "package teprinciple.library;\n\nimport android.content.Context;\nimport android.support.test.InstrumentationRegistry;\nimpor"
  },
  {
    "path": "updateapputils/src/main/AndroidManifest.xml",
    "chars": 1509,
    "preview": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          xmlns:tools=\"http://schemas.android.com/t"
  },
  {
    "path": "updateapputils/src/main/java/constant/DownLoadBy.kt",
    "chars": 209,
    "preview": "package constant\n\n/**\n * desc: 下载方式\n * time: 2019/6/18\n * @author yk\n */\nobject DownLoadBy {\n    /**\n     * app下载\n     *"
  },
  {
    "path": "updateapputils/src/main/java/constant/UiType.kt",
    "chars": 278,
    "preview": "package constant\n\n/**\n * desc: UI 类型\n * time: 2019/6/27\n * @author yk\n */\nobject UiType {\n\n    /**\n     * 简洁版\n     */\n  "
  },
  {
    "path": "updateapputils/src/main/java/extension/BooleanKtx.kt",
    "chars": 593,
    "preview": "package extension\n\nimport kotlin.contracts.ExperimentalContracts\nimport kotlin.contracts.InvocationKind\nimport kotlin.co"
  },
  {
    "path": "updateapputils/src/main/java/extension/ContextKtx.kt",
    "chars": 1485,
    "preview": "package extension\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.ConnectivityManager\ni"
  },
  {
    "path": "updateapputils/src/main/java/extension/CoreKtx.kt",
    "chars": 1273,
    "preview": "package extension\n\nimport android.app.ActivityManager\nimport android.content.Context\nimport android.os.Build\nimport andr"
  },
  {
    "path": "updateapputils/src/main/java/extension/StringKtx.kt",
    "chars": 354,
    "preview": "package extension\n\nimport java.io.File\n\n/**\n * desc: string 相关扩展\n * author: teprinciple on 2020/3/27.\n */\n\n/**\n * 根据文件路径"
  },
  {
    "path": "updateapputils/src/main/java/listener/Md5CheckResultListener.kt",
    "chars": 157,
    "preview": "package listener\n\n/**\n * desc: Md5校验结果回调\n * time: 2019/6/21\n * @author teprinciple\n */\ninterface Md5CheckResultListener "
  },
  {
    "path": "updateapputils/src/main/java/listener/OnBtnClickListener.kt",
    "chars": 194,
    "preview": "package listener\n\n/**\n * desc: 按钮点击监听\n * time: 2019/9/16\n * @author teprinciple\n */\ninterface OnBtnClickListener {\n\n    "
  },
  {
    "path": "updateapputils/src/main/java/listener/OnInitUiListener.kt",
    "chars": 409,
    "preview": "package listener\n\nimport android.view.View\nimport model.UiConfig\nimport model.UpdateConfig\n\n/**\n * desc: 初始化UI 回调 用于进一步自"
  },
  {
    "path": "updateapputils/src/main/java/listener/UpdateDownloadListener.kt",
    "chars": 368,
    "preview": "package listener\n\n/**\n * desc: 下载监听\n * time: 2019/6/19\n * @author teprinciple\n */\ninterface UpdateDownloadListener {\n\n  "
  },
  {
    "path": "updateapputils/src/main/java/model/UiConfig.kt",
    "chars": 1350,
    "preview": "package model\n\nimport com.teprinciple.updateapputils.R\nimport constant.UiType\nimport extension.string\n\n/**\n * desc: UiCo"
  },
  {
    "path": "updateapputils/src/main/java/model/UpdateConfig.kt",
    "chars": 967,
    "preview": "package model\n\nimport constant.DownLoadBy\n\ndata class UpdateConfig(\n    var isDebug: Boolean = true, // 是否是调试模式,调试模式会输出日"
  },
  {
    "path": "updateapputils/src/main/java/model/UpdateInfo.kt",
    "chars": 501,
    "preview": "package model\n\nimport com.teprinciple.updateapputils.R\nimport extension.string\n\n/**\n * desc: UpdateInfo\n * time: 2019/6/"
  },
  {
    "path": "updateapputils/src/main/java/ui/UpdateAppActivity.kt",
    "chars": 10146,
    "preview": "package ui\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android."
  },
  {
    "path": "updateapputils/src/main/java/update/DownloadAppUtils.kt",
    "chars": 7491,
    "preview": "package update\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Bu"
  },
  {
    "path": "updateapputils/src/main/java/update/UpdateAppReceiver.kt",
    "chars": 4912,
    "preview": "package update\n\nimport android.app.Notification\nimport android.app.NotificationChannel\nimport android.app.NotificationMa"
  },
  {
    "path": "updateapputils/src/main/java/update/UpdateAppService.kt",
    "chars": 845,
    "preview": "package update\n\nimport android.app.Service\nimport android.content.Intent\nimport android.content.IntentFilter\nimport andr"
  },
  {
    "path": "updateapputils/src/main/java/update/UpdateAppUtils.kt",
    "chars": 4209,
    "preview": "package update\n\nimport android.content.Context\nimport extension.globalContext\nimport extension.log\nimport extension.no\ni"
  },
  {
    "path": "updateapputils/src/main/java/update/UpdateFileProvider.kt",
    "chars": 579,
    "preview": "package update\n\nimport android.support.v4.content.FileProvider\nimport extension.log\nimport extension.yes\nimport util.Glo"
  },
  {
    "path": "updateapputils/src/main/java/util/AlertDialogUtil.kt",
    "chars": 1007,
    "preview": "package util\n\nimport android.app.Activity\nimport android.app.AlertDialog\nimport com.teprinciple.updateapputils.R\nimport "
  },
  {
    "path": "updateapputils/src/main/java/util/FileDownloadUtil.kt",
    "chars": 4040,
    "preview": "package util\n\nimport extension.log\nimport extension.no\nimport extension.yes\nimport kotlinx.coroutines.Dispatchers\nimport"
  },
  {
    "path": "updateapputils/src/main/java/util/GlobalContextProvider.kt",
    "chars": 270,
    "preview": "package util\n\nimport android.annotation.SuppressLint\nimport android.content.Context\n\n/**\n * desc: 提供context.\n */\n@Suppre"
  },
  {
    "path": "updateapputils/src/main/java/util/SPUtil.kt",
    "chars": 1406,
    "preview": "package util\n\nimport android.app.Activity\nimport android.content.SharedPreferences\nimport extension.globalContext\n\n/**\n "
  },
  {
    "path": "updateapputils/src/main/java/util/SignMd5Util.kt",
    "chars": 3548,
    "preview": "package util\n\nimport android.content.pm.PackageManager\nimport android.content.pm.Signature\nimport extension.globalContex"
  },
  {
    "path": "updateapputils/src/main/res/anim/dialog_enter.xml",
    "chars": 212,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <alpha\n     "
  },
  {
    "path": "updateapputils/src/main/res/anim/dialog_out.xml",
    "chars": 212,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <alpha\n     "
  },
  {
    "path": "updateapputils/src/main/res/drawable/bg_update_btn.xml",
    "chars": 187,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <corners a"
  },
  {
    "path": "updateapputils/src/main/res/drawable/bg_update_dialog.xml",
    "chars": 222,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:sha"
  },
  {
    "path": "updateapputils/src/main/res/layout/view_update_dialog_plentiful.xml",
    "chars": 3625,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<android.support.constraint.ConstraintLayout\n    xmlns:android=\"http://schemas.an"
  },
  {
    "path": "updateapputils/src/main/res/layout/view_update_dialog_simple.xml",
    "chars": 2667,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    "
  },
  {
    "path": "updateapputils/src/main/res/values/colors.xml",
    "chars": 240,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"white\">#ffffff</color>\n    <color name=\"text_blue\">#"
  },
  {
    "path": "updateapputils/src/main/res/values/strings.xml",
    "chars": 655,
    "preview": "<resources>\n    <string name=\"notice\">提示</string>\n    <string name=\"cancel\">取消</string>\n    <string name=\"sure\">确认</stri"
  },
  {
    "path": "updateapputils/src/main/res/values/styles.xml",
    "chars": 1589,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"CustomDialog\" parent=\"@android:style/Theme.Dialog\">\n"
  },
  {
    "path": "updateapputils/src/main/res/values-en/strings.xml",
    "chars": 793,
    "preview": "<resources>\n    <string name=\"notice\">Notice</string>\n    <string name=\"cancel\">Cancel</string>\n    <string name=\"sure\">"
  },
  {
    "path": "updateapputils/src/main/res/xml/network_security_config.xml",
    "chars": 144,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <base-config cleartextTrafficPermitted=\"true\" />\n</"
  },
  {
    "path": "updateapputils/src/main/res/xml/update_file_paths.xml",
    "chars": 104,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n    <external-path path=\"\" name=\"files_root\" />\n</paths>\n"
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the teprinciple/UpdateAppDemo GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 75 files (148.8 KB), approximately 39.4k tokens, and a symbol index with 100 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!