[
  {
    "path": ".github/workflows/android.yml",
    "content": "# workflow的名称，会显示在github 的项目的Actions的右边列表中，如下图\nname: Android CI\n\n# 在满足以下条件触发这个workflow\non:\n  push:\n    # 在指定的远程分支 master上，发生推送时\n    branches: [ master ]\n\njobs:\n  # 多个job，如果有多个，每个以“-”开头，这里只有一个 job\n  build:\n\n    runs-on: ubuntu-latest  # 该job 运行的系统环境，支持ubuntu 、windows、macOS\n\n    # 下面是多个step ，每个以“-”开头\n    steps:\n      - uses: actions/checkout@v2       # step：检查分支\n      - name: set up JDK 1.8            # step：设置jdk版本\n        uses: actions/setup-java@v1     # 引用公共action\n        with:\n          java-version: 1.8\n      - name: Build with Gradle         # step：打包apk\n        # 运行打包命令\n        run: chmod +x gradlew &&./gradlew assembleRelease\n\n        #step：上传apk 到action，在右上角查看\n        # 官方文档 https://help.github.com/cn/actions/automating-your-workflow-with-github-actions/persisting-workflow-data-using-artifacts#uploading-build-and-test-artifacts\n      - name: Upload APK\n        uses: actions/upload-artifact@v1\n        with:\n            name: app\n            path: app/build/outputs/apk/release/app-release.apk\n\n"
  },
  {
    "path": ".gitignore",
    "content": "*.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",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping directory=\"$PROJECT_DIR$\" vcs=\"Git\" />\n  </component>\n</project>"
  },
  {
    "path": "README.md",
    "content": "# UpdateAppUtils2.0\n\n [ ![](https://img.shields.io/badge/platform-android-green.svg) ](http://developer.android.com/index.html) \n [ ![Download](https://api.bintray.com/packages/teprinciple/maven/updateapputils/images/download.svg) ](https://bintray.com/teprinciple/maven/updateapputils/_latestVersion)\n\n### 一行代码，快速实现app在线下载更新  A simple library for Android update app\n\n#### UpdateAppUtils2.0 特点\n* Kotlin First，Kotlin开发\n* 支持AndroidX\n* 支持Md5签名验证\n* 支持自定义任意UI\n* 适配中英文\n* 适配至Android 10\n* 通知栏图片自定义\n* 支持修改是否每次显示弹窗（非强更）\n* 支持安装完成后自动删除安装包\n\nUpdateAppUtils2.0功能结构变化巨大，建议使用2.0以上版本；[2.0以前版本文档](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/README_1.5.2.md)\n\n#### 效果图\n<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\">\n<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\">\n\n### 集成\n\n```\nrepositories {\n   jcenter()\n}\n```\n\nSupport\n```\nimplementation 'com.teprinciple:updateapputils:2.3.0'\n```\n\nAndroidX项目\n```\n注意，由于操作失误bintray 中updateapputilsX被我删掉，\n所以2.3.0以后使用updateapputilsx。之前的仍使用updateapputilsX\n//implementation 'com.teprinciple:updateapputilsX:2.2.1'\nimplementation 'com.teprinciple:updateapputilsx:2.3.0'\n\n```\n\n### 使用\n下面为kotlin使用示例，Java示例请参考[JavaDemo](https://github.com/teprinciple/UpdateAppUtils/blob/master/app/src/main/java/com/example/teprinciple/updateappdemo/JavaDemoActivity.java)\n#### 1、快速使用\n\n##### 注意：部分手机SDK内部初始化不了context，造成context空指针，建议在application或者使用SDK前先初始化\n```\n UpdateAppUtils.init(context)\n```\n\n```\n UpdateAppUtils\n        .getInstance()\n        .apkUrl(apkUrl)\n        .updateTitle(updateTitle)\n        .updateContent(updateContent)\n        .update()\n```\n\n#### 2、多配置使用\n```\n    // ui配置\n    val uiConfig = UiConfig().apply {\n        uiType = UiType.PLENTIFUL\n        cancelBtnText = \"下次再说\"\n        updateLogoImgRes = R.drawable.ic_update\n        updateBtnBgRes = R.drawable.bg_btn\n        titleTextColor = Color.BLACK\n        titleTextSize = 18f\n        contentTextColor = Color.parseColor(\"#88e16531\")\n    }\n\n    // 更新配置\n    val updateConfig = UpdateConfig().apply {\n        force = true\n        checkWifi = true\n        needCheckMd5 = true\n        isShowNotification = true\n        notifyImgRes = R.drawable.ic_logo\n        apkSavePath = Environment.getExternalStorageDirectory().absolutePath +\"/teprinciple\"\n        apkSaveName = \"teprinciple\"\n    }\n\n    UpdateAppUtils\n        .getInstance()\n        .apkUrl(apkUrl)\n        .updateTitle(updateTitle)\n        .updateContent(updateContent)\n        .updateConfig(updateConfig)\n        .uiConfig(uiConfig)\n        .setUpdateDownloadListener(object : UpdateDownloadListener{\n            // do something\n        })\n        .update()\n```\n#### 3、md5校验说明\n 为了保证app的安全性，避免apk被二次打包的风险。UpdateAppUtils内部提供了对签名文件md5值校验的接口；\n 首先你需要保证当前应用和服务器apk使用同一个签名文件进行了签名（一定要保管好自己的签名文件，否则检验就失去了意义），\n 然后你需要将UpdateConfig 的 needCheckMd5 设置为true，并在Md5CheckResultListener监听中，监听校验返回结果。具体使用可参考\n [CheckMd5DemoActivity](https://github.com/teprinciple/UpdateAppUtils/blob/master/app/src/main/java/com/example/teprinciple/updateappdemo/CheckMd5DemoActivity.kt)\n ```\n UpdateAppUtils\n        .getInstance()\n        .apkUrl(apkUrl)\n        .updateTitle(updateTitle)\n        .updateContent(updateContent)\n        .updateConfig(updateConfig) // needCheckMd5设置为true\n        .setMd5CheckResultListener(object : Md5CheckResultListener{\n            override fun onResult(result: Boolean) {\n                // true：检验通过，false：检验失败\n            }\n        })\n ```\n\n#### 4、自定义UI\n UpdateAppUtils内置了两套UI，你可以通过修改[UiConfig](#UiConfig)进行UI内容的自定义；\n 当然当内部UI模板与你期望UI差别很大时，你可以采用[完全自定义UI](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/%E8%87%AA%E5%AE%9A%E4%B9%89UI.md)\n\n### Api说明\n#### 1、UpdateAppUtils Api\n\n| api             | 说明                               | 默认值                   | 必须设置 |\n|:-------------- |:------------------------------------ |:--------------------- |:------ |\n| fun apkUrl(apkUrl: String)| 更新包服务器url         | null                  | true   |\n| fun update() | UpdateAppUtils入口      | -     | true   |\n| fun updateTitle(title: String)         | 更新标题     | 版本更新啦！     | false   |\n| fun updateContent(content: String)        | 更新内容   | 发现新版本，立即更新  | false   |\n| fun updateConfig(config: UpdateConfig)   | 更新配置  | 查看源码 | false  |\n| fun uiConfig(uiConfig: UiConfig) | 更新弹窗UI配置  | 查看源码                 | false  |\n| fun setUpdateDownloadListener() | 下载过程监听  | null | false   |\n| fun setMd5CheckResultListener() | md5校验结果回调 | null  | false  |\n| fun setOnInitUiListener() | 初始化更新弹窗UI回调 | null  | false  |\n| fun deleteInstalledApk() | 删除已安装的apk | -  | false  |\n| fun setCancelBtnClickListener() | 暂不更新按钮点击监听 | -  | false  |\n| fun setUpdateBtnClickListener() | 立即更新按钮点击监听 | -  | false  |\n\n#### 2、UpdateConfig：更新配置说明\n\n| 属性                  | 说明                               | 默认值 |\n|:--------------------- |:------------------------------------ |:------ |\n| isDebug               | 是否输出【UpdateAppUtils】为Tag的日志|  true |\n| force                 | 是否强制更新，强制时无取消按钮       | false  |\n| apkSavePath           | apk下载存放位置               | 包名目录    |\n| apkSaveName           | apk保存文件名                 | 项目名        |\n| downloadBy            | 下载方式              | DownLoadBy.APP   |\n| needCheckMd5          | 是否需要校验apk签名md5              | false   |\n| checkWifi             | 检查是否wifi        | false   |\n| isShowNotification    | 是否显示通知栏进度    | true   |\n| notifyImgRes          | 通知栏图标              | 项目icon  |\n| serverVersionName     | 服务器上apk版本名 | 无   |\n| serverVersionCode     | 服务器上apk版本号 | 无   |\n| alwaysShow            | 是否每次显示更新弹窗（非强更） | true   |\n| thisTimeShow          | 本次是否显示更新弹窗（非强更） | false  |\n| alwaysShowDownLoadDialog| 是否需要显示更新下载进度弹窗（非强更） | false  |\n| showDownloadingToast  | 开始下载时是否显示Toast | true  |\n\n#### 3、UiConfig：更新弹窗Ui配置说明 <div id = \"UiConfig\"/>\n\n| 属性                  | 说明                               | 默认值 |\n|:--------------------- |:------------------------------------ |:------ |\n| uiType                | ui模板                        | UiType.SIMPLE |\n| customLayoutId        | 自定义布局id    | false  |\n| updateLogoImgRes      | 更新弹窗logo图片资源id               | -    |\n| titleTextSize         | 标题字体大小                 | 16sp        |\n| titleTextColor        | 标题字体颜色              | -   |\n| contentTextSize         | 内容字体大小                 | 14sp       |\n| contentTextColor        | 内容字体颜色              | -  |\n| updateBtnBgColor      | 更新按钮背景颜色              | -   |\n| updateBtnBgRes        | 更新按钮背景资源id        | -   |\n| updateBtnTextColor    | 更新按钮字体颜色    | -   |\n| updateBtnTextSize     | 更新按钮文字大小 | 14sp   |\n| updateBtnText         | 更新按钮文案              | 立即更新  |\n| cancelBtnBgColor      | 取消按钮背景颜色 | -   |\n| cancelBtnBgRes        | 取消按钮背景资源id | -   |\n| cancelBtnTextColor    | 取消按钮文字颜色 | -   |\n| cancelBtnTextSize     | 取消按钮文字大小 | 14sp   |\n| cancelBtnText         | 取消按钮文案 | 暂不更新   |\n| downloadingToastText  | 开始下载时的Toast提示文字 | 更新下载中...   |\n| downloadingBtnText    | 下载中 下载按钮以及通知栏标题前缀，进度自动拼接在后面 | 下载中   |\n| downloadFailText      | 下载出错时，下载按钮及通知栏标题 | 下载出错，点击重试   |\n\n### Demo体验\n<img src=\"https://github.com/teprinciple/UpdateAppUtils/blob/master/img/demo.png\" width=\"220\">\n\n### 更新日志\n\n#### 2.3.0\n* 修复部分手机context空指针异常\n##### [更多历史版本](https://github.com/teprinciple/UpdateAppUtils/blob/master/readme/version.md)"
  },
  {
    "path": "app/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "app/build.gradle",
    "content": "apply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\napply plugin: 'kotlin-android-extensions'\n\nandroid {\n    compileSdkVersion 29\n    defaultConfig {\n        applicationId \"com.example.teprinciple.updateappdemo\"\n        minSdkVersion 19\n        targetSdkVersion 29\n        versionCode 203\n        versionName \"2.0.3\"\n    }\n    signingConfigs {\n        config {\n            storeFile file('../update.jks')\n            storePassword '123456'\n            keyAlias 'update'\n            keyPassword '123456'\n        }\n    }\n\n    buildTypes {\n        release {\n            minifyEnabled false\n            signingConfig signingConfigs.debug\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n\n        debug {\n            minifyEnabled false\n            signingConfig signingConfigs.config\n        }\n    }\n\n    lintOptions {\n        abortOnError false\n    }\n}\n\ndependencies {\n    implementation fileTree(include: ['*.jar'], dir: 'libs')\n    implementation project(':updateapputils')\n    implementation 'com.android.support:appcompat-v7:28.0.0'\n    implementation 'com.android.support.constraint:constraint-layout:1.1.3'\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n\n    debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.1'\n}\n"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in C:\\Users\\Teprinciple\\AppData\\Local\\Android\\Sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n"
  },
  {
    "path": "app/src/androidTest/java/com/example/teprinciple/updateappdemo/ExampleInstrumentedTest.java",
    "content": "package com.example.teprinciple.updateappdemo;\n\nimport android.content.Context;\nimport android.support.test.InstrumentationRegistry;\nimport android.support.test.runner.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport static org.junit.Assert.*;\n\n/**\n * Instrumentation test, which will execute on an Android device.\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\n@RunWith(AndroidJUnit4.class)\npublic class ExampleInstrumentedTest {\n    @Test\n    public void useAppContext() throws Exception {\n        // Context of the app under test.\n        Context appContext = InstrumentationRegistry.getTargetContext();\n\n        assertEquals(\"com.example.teprinciple.updateappdemo\", appContext.getPackageName());\n    }\n}\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    package=\"com.example.teprinciple.updateappdemo\">\n\n    <application\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/AppTheme\"\n        tools:ignore=\"GoogleAppIndexingWarning\">\n\n        <activity android:name=\".MainActivity\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\"/>\n\n                <category android:name=\"android.intent.category.LAUNCHER\"/>\n            </intent-filter>\n        </activity>\n\n        <activity android:name=\".JavaDemoActivity\"/>\n        <activity android:name=\".CheckMd5DemoActivity\"/>\n\n    </application>\n\n</manifest>"
  },
  {
    "path": "app/src/main/java/com/example/teprinciple/updateappdemo/CheckMd5DemoActivity.kt",
    "content": "package com.example.teprinciple.updateappdemo\n\nimport android.os.Bundle\nimport android.os.Environment\nimport android.support.v7.app.AppCompatActivity\nimport android.widget.Toast\nimport kotlinx.android.synthetic.main.check_md5_demo_activity.*\nimport listener.Md5CheckResultListener\nimport model.UpdateConfig\nimport update.UpdateAppUtils\n\n/**\n * desc: md5校验示例\n * time: 2019/7/1\n * @author yk\n */\nclass CheckMd5DemoActivity : AppCompatActivity() {\n\n    /**\n     * 已签名的apk\n     */\n    private val signedApkUrl = \"http://118.24.148.250:8080/yk/update_signed.apk\"\n\n    /**\n     * 非正规签名的apk\n     */\n    private val notSignedApkUrl = \"http://118.24.148.250:8080/yk/update_not_signed.apk\"\n\n    private val updateTitle = \"发现新版本V2.0.0\"\n    private val updateContent = \"1、Kotlin重构版\\n2、支持自定义UI\\n3、增加md5校验\\n4、更多功能等你探索\"\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        setContentView(R.layout.check_md5_demo_activity)\n\n        // 更新配置\n        val updateConfig = UpdateConfig().apply {\n            force = true\n            needCheckMd5 = true\n        }\n\n        // 正确签名\n        btn_signed.setOnClickListener {\n            updateConfig.apply { apkSaveName = \"signed\" }\n            UpdateAppUtils\n                .getInstance()\n                .apkUrl(signedApkUrl)\n                .updateTitle(updateTitle)\n                .updateContent(updateContent)\n                .updateConfig(updateConfig)\n                .setMd5CheckResultListener(object : Md5CheckResultListener {\n                    override fun onResult(result: Boolean) {\n                        Toast.makeText(this@CheckMd5DemoActivity, \"Md5检验是否通过：$result\", Toast.LENGTH_SHORT).show()\n                    }\n                })\n                .update()\n        }\n\n        // 错误签名\n        btn_not_signed.setOnClickListener {\n            updateConfig.apply { apkSaveName = \"not_signed\" }\n            UpdateAppUtils\n                .getInstance()\n                .apkUrl(notSignedApkUrl)\n                .updateTitle(updateTitle)\n                .updateContent(updateContent)\n                .updateConfig(updateConfig)\n                .setMd5CheckResultListener(object : Md5CheckResultListener {\n                    override fun onResult(result: Boolean) {\n                        Toast.makeText(this@CheckMd5DemoActivity, \"Md5检验是否通过：$result\", Toast.LENGTH_SHORT).show()\n                    }\n                })\n                .update()\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/example/teprinciple/updateappdemo/JavaDemoActivity.java",
    "content": "package com.example.teprinciple.updateappdemo;\n\nimport android.os.Bundle;\nimport android.support.annotation.Nullable;\nimport android.support.v7.app.AppCompatActivity;\nimport android.view.View;\n\nimport org.jetbrains.annotations.NotNull;\n\nimport constant.UiType;\nimport listener.Md5CheckResultListener;\nimport listener.UpdateDownloadListener;\nimport model.UiConfig;\nimport model.UpdateConfig;\nimport update.UpdateAppUtils;\n\n/**\n * desc: java使用实例\n * time: 2019/6/27\n * @author yk\n */\npublic class JavaDemoActivity extends AppCompatActivity {\n\n    private String apkUrl = \"http://118.24.148.250:8080/yk/update_signed.apk\";\n    private String updateTitle = \"发现新版本V2.0.0\";\n    private String updateContent = \"1、Kotlin重构版\\n2、支持自定义UI\\n3、增加md5校验\\n4、更多功能等你探索\";\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        setContentView(R.layout.activity_java_demo);\n\n        UpdateAppUtils.init(this);\n\n        findViewById(R.id.btn_java).setOnClickListener(new View.OnClickListener() {\n            @Override\n            public void onClick(View v) {\n\n                UpdateConfig updateConfig = new UpdateConfig();\n                updateConfig.setCheckWifi(true);\n                updateConfig.setNeedCheckMd5(true);\n                updateConfig.setNotifyImgRes(R.drawable.ic_logo);\n\n                UiConfig uiConfig = new UiConfig();\n                uiConfig.setUiType(UiType.PLENTIFUL);\n\n                UpdateAppUtils\n                        .getInstance()\n                        .apkUrl(apkUrl)\n                        .updateTitle(updateTitle)\n                        .updateContent(updateContent)\n                        .uiConfig(uiConfig)\n                        .updateConfig(updateConfig)\n                        .setMd5CheckResultListener(new Md5CheckResultListener() {\n                            @Override\n                            public void onResult(boolean result) {\n\n                            }\n                        })\n                        .setUpdateDownloadListener(new UpdateDownloadListener() {\n                            @Override\n                            public void onStart() {\n\n                            }\n\n                            @Override\n                            public void onDownload(int progress) {\n\n                            }\n\n                            @Override\n                            public void onFinish() {\n\n                            }\n\n                            @Override\n                            public void onError(@NotNull Throwable e) {\n\n                            }\n                        })\n                        .update();\n            }\n        });\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/example/teprinciple/updateappdemo/MainActivity.kt",
    "content": "package com.example.teprinciple.updateappdemo\n\nimport android.content.Intent\nimport android.graphics.Color\nimport android.os.Bundle\nimport android.os.Environment\nimport android.support.v7.app.AppCompatActivity\nimport android.view.View\nimport android.widget.TextView\nimport android.widget.Toast\nimport constant.DownLoadBy\nimport constant.UiType\nimport kotlinx.android.synthetic.main.activity_main.*\nimport listener.OnBtnClickListener\nimport listener.OnInitUiListener\nimport listener.UpdateDownloadListener\nimport model.UiConfig\nimport model.UpdateConfig\nimport update.UpdateAppUtils\n\n\nclass MainActivity : AppCompatActivity() {\n\n    private val apkUrl = \"http://118.24.148.250:8080/yk/update_signed.apk\"\n//    private val apkUrl = \"https://github.com/AlexLiuSheng/CheckVersionLib/blob/master/library/src/main/java/com/allenliu/versionchecklib/utils/AppUtils.java\"\n    private val updateTitle = \"发现新版本V2.0.0\"\n    private val updateContent = \"1、Kotlin重构版\\n2、支持自定义UI\\n3、增加md5校验\\n4、更多功能等你探索\"\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n        setContentView(R.layout.activity_main)\n\n        UpdateAppUtils.init(this)\n\n        // 基本使用\n        btn_basic_use.setOnClickListener {\n            UpdateAppUtils\n                .getInstance()\n                .apkUrl(apkUrl)\n                .updateTitle(updateTitle)\n                .updateConfig(UpdateConfig(apkSaveName = \"up_1.1\"))\n                .uiConfig(UiConfig(uiType = UiType.SIMPLE))\n                .updateContent(updateContent)\n                .update()\n        }\n\n        // 浏览器下载\n        btn_download_by_browser.setOnClickListener {\n\n            // 使用SpannableString\n            val content = SpanUtils(this)\n                .appendLine(\"1、Kotlin重构版\")\n                .appendLine(\"2、支持自定义UI\").setForegroundColor(Color.RED)\n                .appendLine(\"3、增加md5校验\").setForegroundColor(Color.parseColor(\"#88e16531\")).setFontSize(20, true)\n                .appendLine(\"4、更多功能等你探索\").setBoldItalic()\n                .appendLine().appendImage(R.mipmap.ic_launcher).setBoldItalic()\n                .create()\n\n            UpdateAppUtils\n                .getInstance()\n                .apkUrl(apkUrl)\n                .updateTitle(updateTitle)\n                .updateContent(content)\n                .updateConfig(UpdateConfig().apply {\n                    downloadBy = DownLoadBy.BROWSER\n//                     alwaysShow = false\n                    serverVersionName = \"2.0.0\"\n                })\n                .uiConfig(UiConfig(uiType = UiType.PLENTIFUL))\n\n                // 设置 取消 按钮点击事件\n                .setCancelBtnClickListener(object : OnBtnClickListener {\n                    override fun onClick(): Boolean {\n                        Toast.makeText(this@MainActivity, \"cancel btn click\", Toast.LENGTH_SHORT).show()\n                        return false // 事件是否消费，是否需要传递下去。false-会执行原有点击逻辑，true-只执行本次设置的点击逻辑\n                    }\n                })\n\n                // 设置 立即更新 按钮点击事件\n                .setUpdateBtnClickListener(object : OnBtnClickListener {\n                    override fun onClick(): Boolean {\n                        Toast.makeText(this@MainActivity, \"update btn click\", Toast.LENGTH_SHORT).show()\n                        return false // 事件是否消费，是否需要传递下去。false-会执行原有点击逻辑，true-只执行本次设置的点击逻辑\n                    }\n                })\n\n                .update()\n        }\n\n        // 自定义UI\n        btn_custom_ui.setOnClickListener {\n            UpdateAppUtils\n                .getInstance()\n                .apkUrl(apkUrl)\n                .updateTitle(updateTitle)\n                .updateContent(updateContent)\n                .updateConfig(UpdateConfig(alwaysShowDownLoadDialog = true))\n                .uiConfig(UiConfig(uiType = UiType.CUSTOM, customLayoutId = R.layout.view_update_dialog_custom))\n                .setOnInitUiListener(object : OnInitUiListener {\n                    override fun onInitUpdateUi(view: View?, updateConfig: UpdateConfig, uiConfig: UiConfig) {\n                        view?.findViewById<TextView>(R.id.tv_update_title)?.text = \"版本更新啦\"\n                        view?.findViewById<TextView>(R.id.tv_version_name)?.text = \"V2.0.0\"\n                        // do more...\n                    }\n                })\n                .update()\n        }\n\n        // java使用示例\n        btn_java_sample.setOnClickListener {\n            startActivity(Intent(this, JavaDemoActivity::class.java))\n        }\n\n        // md5校验\n        btn_check_md5.setOnClickListener {\n            startActivity(Intent(this, CheckMd5DemoActivity::class.java))\n        }\n\n        // 高级使用\n        btn_higher_level_use.setOnClickListener {\n            // ui配置\n            val uiConfig = UiConfig().apply {\n                uiType = UiType.PLENTIFUL\n                cancelBtnText = \"下次再说\"\n                updateLogoImgRes = R.drawable.ic_update\n                updateBtnBgRes = R.drawable.bg_btn\n                titleTextColor = Color.BLACK\n                titleTextSize = 18f\n                contentTextColor = Color.parseColor(\"#88e16531\")\n            }\n\n            // 更新配置\n            val updateConfig = UpdateConfig().apply {\n                force = true\n                isDebug = true\n                checkWifi = true\n                isShowNotification = true\n                notifyImgRes = R.drawable.ic_logo\n                apkSavePath = Environment.getExternalStorageDirectory().absolutePath + \"/teprinciple\"\n                apkSaveName = \"teprinciple\"\n            }\n\n            UpdateAppUtils\n                .getInstance()\n                .apkUrl(apkUrl)\n                .updateTitle(updateTitle)\n                .updateContent(updateContent)\n                .updateConfig(updateConfig)\n                .uiConfig(uiConfig)\n                .setUpdateDownloadListener(object : UpdateDownloadListener {\n                    override fun onStart() {\n                    }\n\n                    override fun onDownload(progress: Int) {\n                    }\n\n                    override fun onFinish() {\n                    }\n\n                    override fun onError(e: Throwable) {\n                    }\n                })\n                .update()\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/example/teprinciple/updateappdemo/SpanUtils.java",
    "content": "package com.example.teprinciple.updateappdemo;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.graphics.BitmapFactory;\nimport android.graphics.BlurMaskFilter;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.Path;\nimport android.graphics.Rect;\nimport android.graphics.Shader;\nimport android.graphics.Typeface;\nimport android.graphics.drawable.BitmapDrawable;\nimport android.graphics.drawable.Drawable;\nimport android.net.Uri;\nimport android.support.annotation.ColorInt;\nimport android.support.annotation.DrawableRes;\nimport android.support.annotation.FloatRange;\nimport android.support.annotation.IntDef;\nimport android.support.annotation.IntRange;\nimport android.support.annotation.NonNull;\nimport android.support.annotation.Nullable;\nimport android.support.v4.content.ContextCompat;\nimport android.text.Layout;\nimport android.text.Layout.Alignment;\nimport android.text.SpannableStringBuilder;\nimport android.text.Spanned;\nimport android.text.TextPaint;\nimport android.text.style.AbsoluteSizeSpan;\nimport android.text.style.AlignmentSpan;\nimport android.text.style.BackgroundColorSpan;\nimport android.text.style.CharacterStyle;\nimport android.text.style.ClickableSpan;\nimport android.text.style.ForegroundColorSpan;\nimport android.text.style.LeadingMarginSpan;\nimport android.text.style.LineHeightSpan;\nimport android.text.style.MaskFilterSpan;\nimport android.text.style.RelativeSizeSpan;\nimport android.text.style.ReplacementSpan;\nimport android.text.style.ScaleXSpan;\nimport android.text.style.StrikethroughSpan;\nimport android.text.style.StyleSpan;\nimport android.text.style.SubscriptSpan;\nimport android.text.style.SuperscriptSpan;\nimport android.text.style.TypefaceSpan;\nimport android.text.style.URLSpan;\nimport android.text.style.UnderlineSpan;\nimport android.text.style.UpdateAppearance;\nimport android.util.Log;\n\nimport java.io.InputStream;\nimport java.lang.annotation.Retention;\nimport java.lang.annotation.RetentionPolicy;\nimport java.lang.ref.WeakReference;\n\nimport static android.graphics.BlurMaskFilter.Blur;\n\n/**\n * <pre>\n *     author: Blankj\n *     blog  : http://blankj.com\n *     usage : https://www.jianshu.com/p/509b0d2626f4\n *     time  : 16/12/13\n *     desc  : utils about span\n * </pre>\n */\npublic final class SpanUtils {\n\n    private static final int COLOR_DEFAULT = 0xFEFFFFFF;\n\n    public static final int ALIGN_BOTTOM = 0;\n    public static final int ALIGN_BASELINE = 1;\n    public static final int ALIGN_CENTER = 2;\n    public static final int ALIGN_TOP = 3;\n\n    @IntDef({ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER, ALIGN_TOP})\n    @Retention(RetentionPolicy.SOURCE)\n    public @interface Align {\n    }\n\n    private static final String LINE_SEPARATOR = System.getProperty(\"line.separator\");\n\n    private CharSequence mText;\n    private int flag;\n    private int foregroundColor;\n    private int backgroundColor;\n    private int lineHeight;\n    private int alignLine;\n    private int quoteColor;\n    private int stripeWidth;\n    private int quoteGapWidth;\n    private int first;\n    private int rest;\n    private int bulletColor;\n    private int bulletRadius;\n    private int bulletGapWidth;\n    private int fontSize;\n    private boolean fontSizeIsDp;\n    private float proportion;\n    private float xProportion;\n    private boolean isStrikethrough;\n    private boolean isUnderline;\n    private boolean isSuperscript;\n    private boolean isSubscript;\n    private boolean isBold;\n    private boolean isItalic;\n    private boolean isBoldItalic;\n    private String fontFamily;\n    private Typeface typeface;\n    private Alignment alignment;\n    private ClickableSpan clickSpan;\n    private String url;\n    private float blurRadius;\n    private Blur style;\n    private Shader shader;\n    private float shadowRadius;\n    private float shadowDx;\n    private float shadowDy;\n    private int shadowColor;\n    private Object[] spans;\n\n    private Bitmap imageBitmap;\n    private Drawable imageDrawable;\n    private Uri imageUri;\n    private int imageResourceId;\n    private int alignImage;\n\n    private int spaceSize;\n    private int spaceColor;\n\n    private SpannableStringBuilder mBuilder;\n\n    private int mType;\n    private final int mTypeCharSequence = 0;\n    private final int mTypeImage = 1;\n    private final int mTypeSpace = 2;\n\n    private Context mContext;\n\n    public SpanUtils(Context context) {\n        mContext = context.getApplicationContext();\n        mBuilder = new SpannableStringBuilder();\n        mText = \"\";\n        setDefault();\n    }\n\n    private void setDefault() {\n        flag = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;\n        foregroundColor = COLOR_DEFAULT;\n        backgroundColor = COLOR_DEFAULT;\n        lineHeight = -1;\n        quoteColor = COLOR_DEFAULT;\n        first = -1;\n        bulletColor = COLOR_DEFAULT;\n        fontSize = -1;\n        proportion = -1;\n        xProportion = -1;\n        isStrikethrough = false;\n        isUnderline = false;\n        isSuperscript = false;\n        isSubscript = false;\n        isBold = false;\n        isItalic = false;\n        isBoldItalic = false;\n        fontFamily = null;\n        typeface = null;\n        alignment = null;\n        clickSpan = null;\n        url = null;\n        blurRadius = -1;\n        shader = null;\n        shadowRadius = -1;\n        spans = null;\n\n        imageBitmap = null;\n        imageDrawable = null;\n        imageUri = null;\n        imageResourceId = -1;\n\n        spaceSize = -1;\n    }\n\n    /**\n     * Set the span of flag.\n     *\n     * @param flag\n     *         The flag.\n     *         <ul>\n     *         <li>{@link Spanned#SPAN_INCLUSIVE_EXCLUSIVE}</li>\n     *         <li>{@link Spanned#SPAN_INCLUSIVE_INCLUSIVE}</li>\n     *         <li>{@link Spanned#SPAN_EXCLUSIVE_EXCLUSIVE}</li>\n     *         <li>{@link Spanned#SPAN_EXCLUSIVE_INCLUSIVE}</li>\n     *         </ul>\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setFlag(final int flag) {\n        this.flag = flag;\n        return this;\n    }\n\n    /**\n     * Set the span of foreground's color.\n     *\n     * @param color\n     *         The color of foreground\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setForegroundColor(@ColorInt final int color) {\n        this.foregroundColor = color;\n        return this;\n    }\n\n    /**\n     * Set the span of background's color.\n     *\n     * @param color\n     *         The color of background\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setBackgroundColor(@ColorInt final int color) {\n        this.backgroundColor = color;\n        return this;\n    }\n\n    /**\n     * Set the span of line height.\n     *\n     * @param lineHeight\n     *         The line height, in pixel.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setLineHeight(@IntRange(from = 0) final int lineHeight) {\n        return setLineHeight(lineHeight, ALIGN_CENTER);\n    }\n\n    /**\n     * Set the span of line height.\n     *\n     * @param lineHeight\n     *         The line height, in pixel.\n     * @param align\n     *         The alignment.\n     *         <ul>\n     *         <li>{@link Align#ALIGN_TOP   }</li>\n     *         <li>{@link Align#ALIGN_CENTER}</li>\n     *         <li>{@link Align#ALIGN_BOTTOM}</li>\n     *         </ul>\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setLineHeight(@IntRange(from = 0) final int lineHeight,\n                                   @Align final int align) {\n        this.lineHeight = lineHeight;\n        this.alignLine = align;\n        return this;\n    }\n\n    /**\n     * Set the span of quote's color.\n     *\n     * @param color\n     *         The color of quote\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setQuoteColor(@ColorInt final int color) {\n        return setQuoteColor(color, 2, 2);\n    }\n\n    /**\n     * Set the span of quote's color.\n     *\n     * @param color\n     *         The color of quote.\n     * @param stripeWidth\n     *         The width of stripe, in pixel.\n     * @param gapWidth\n     *         The width of gap, in pixel.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setQuoteColor(@ColorInt final int color,\n                                   @IntRange(from = 1) final int stripeWidth,\n                                   @IntRange(from = 0) final int gapWidth) {\n        this.quoteColor = color;\n        this.stripeWidth = stripeWidth;\n        this.quoteGapWidth = gapWidth;\n        return this;\n    }\n\n    /**\n     * Set the span of leading margin.\n     *\n     * @param first\n     *         The indent for the first line of the paragraph.\n     * @param rest\n     *         The indent for the remaining lines of the paragraph.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setLeadingMargin(@IntRange(from = 0) final int first,\n                                      @IntRange(from = 0) final int rest) {\n        this.first = first;\n        this.rest = rest;\n        return this;\n    }\n\n    /**\n     * Set the span of bullet.\n     *\n     * @param gapWidth\n     *         The width of gap, in pixel.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setBullet(@IntRange(from = 0) final int gapWidth) {\n        return setBullet(0, 3, gapWidth);\n    }\n\n    /**\n     * Set the span of bullet.\n     *\n     * @param color\n     *         The color of bullet.\n     * @param radius\n     *         The radius of bullet, in pixel.\n     * @param gapWidth\n     *         The width of gap, in pixel.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setBullet(@ColorInt final int color,\n                               @IntRange(from = 0) final int radius,\n                               @IntRange(from = 0) final int gapWidth) {\n        this.bulletColor = color;\n        this.bulletRadius = radius;\n        this.bulletGapWidth = gapWidth;\n        return this;\n    }\n\n    /**\n     * Set the span of font's size.\n     *\n     * @param size\n     *         The size of font.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setFontSize(@IntRange(from = 0) final int size) {\n        return setFontSize(size, false);\n    }\n\n    /**\n     * Set the span of size of font.\n     *\n     * @param size\n     *         The size of font.\n     * @param isSp\n     *         True to use sp, false to use pixel.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setFontSize(@IntRange(from = 0) final int size, final boolean isSp) {\n        this.fontSize = size;\n        this.fontSizeIsDp = isSp;\n        return this;\n    }\n\n    /**\n     * Set the span of proportion of font.\n     *\n     * @param proportion\n     *         The proportion of font.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setFontProportion(final float proportion) {\n        this.proportion = proportion;\n        return this;\n    }\n\n    /**\n     * Set the span of transverse proportion of font.\n     *\n     * @param proportion\n     *         The transverse proportion of font.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setFontXProportion(final float proportion) {\n        this.xProportion = proportion;\n        return this;\n    }\n\n    /**\n     * Set the span of strikethrough.\n     *\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setStrikethrough() {\n        this.isStrikethrough = true;\n        return this;\n    }\n\n    /**\n     * Set the span of underline.\n     *\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setUnderline() {\n        this.isUnderline = true;\n        return this;\n    }\n\n    /**\n     * Set the span of superscript.\n     *\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setSuperscript() {\n        this.isSuperscript = true;\n        return this;\n    }\n\n    /**\n     * Set the span of subscript.\n     *\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setSubscript() {\n        this.isSubscript = true;\n        return this;\n    }\n\n    /**\n     * Set the span of bold.\n     *\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setBold() {\n        isBold = true;\n        return this;\n    }\n\n    /**\n     * Set the span of bold.\n     *\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setNotBold() {\n        isBold = false;\n        return this;\n    }\n\n    /**\n     * Set the span of italic.\n     *\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setItalic() {\n        isItalic = true;\n        return this;\n    }\n\n    /**\n     * Set the span of bold italic.\n     *\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setBoldItalic() {\n        isBoldItalic = true;\n        return this;\n    }\n\n    /**\n     * Set the span of font family.\n     *\n     * @param fontFamily\n     *         The font family.\n     *         <ul>\n     *         <li>monospace</li>\n     *         <li>serif</li>\n     *         <li>sans-serif</li>\n     *         </ul>\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setFontFamily(@NonNull final String fontFamily) {\n        this.fontFamily = fontFamily;\n        return this;\n    }\n\n    /**\n     * Set the span of typeface.\n     *\n     * @param typeface\n     *         The typeface.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setTypeface(@NonNull final Typeface typeface) {\n        this.typeface = typeface;\n        return this;\n    }\n\n    /**\n     * Set the span of alignment.\n     *\n     * @param alignment\n     *         The alignment.\n     *         <ul>\n     *         <li>{@link Alignment#ALIGN_NORMAL  }</li>\n     *         <li>{@link Alignment#ALIGN_OPPOSITE}</li>\n     *         <li>{@link Alignment#ALIGN_CENTER  }</li>\n     *         </ul>\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setAlign(@NonNull final Alignment alignment) {\n        this.alignment = alignment;\n        return this;\n    }\n\n    /**\n     * Set the span of click.\n     * <p>Must set {@code view.setMovementMethod(LinkMovementMethod.getInstance())}</p>\n     *\n     * @param clickSpan\n     *         The span of click.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setClickSpan(@NonNull final ClickableSpan clickSpan) {\n        this.clickSpan = clickSpan;\n        return this;\n    }\n\n    /**\n     * Set the span of url.\n     * <p>Must set {@code view.setMovementMethod(LinkMovementMethod.getInstance())}</p>\n     *\n     * @param url\n     *         The url.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setUrl(@NonNull final String url) {\n        this.url = url;\n        return this;\n    }\n\n    /**\n     * Set the span of blur.\n     *\n     * @param radius\n     *         The radius of blur.\n     * @param style\n     *         The style.\n     *         <ul>\n     *         <li>{@link Blur#NORMAL}</li>\n     *         <li>{@link Blur#SOLID}</li>\n     *         <li>{@link Blur#OUTER}</li>\n     *         <li>{@link Blur#INNER}</li>\n     *         </ul>\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setBlur(@FloatRange(from = 0, fromInclusive = false) final float radius,\n                             final Blur style) {\n        this.blurRadius = radius;\n        this.style = style;\n        return this;\n    }\n\n    /**\n     * Set the span of shader.\n     *\n     * @param shader\n     *         The shader.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setShader(@NonNull final Shader shader) {\n        this.shader = shader;\n        return this;\n    }\n\n    /**\n     * Set the span of shadow.\n     *\n     * @param radius\n     *         The radius of shadow.\n     * @param dx\n     *         X-axis offset, in pixel.\n     * @param dy\n     *         Y-axis offset, in pixel.\n     * @param shadowColor\n     *         The color of shadow.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setShadow(@FloatRange(from = 0, fromInclusive = false) final float radius,\n                               final float dx,\n                               final float dy,\n                               final int shadowColor) {\n        this.shadowRadius = radius;\n        this.shadowDx = dx;\n        this.shadowDy = dy;\n        this.shadowColor = shadowColor;\n        return this;\n    }\n\n\n    /**\n     * Set the spans.\n     *\n     * @param spans\n     *         The spans.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils setSpans(@NonNull final Object... spans) {\n        if (spans.length > 0) {\n            this.spans = spans;\n        }\n        return this;\n    }\n\n    /**\n     * Append the text text.\n     *\n     * @param text\n     *         The text.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils append(@NonNull final CharSequence text) {\n        apply(mTypeCharSequence);\n        mText = text;\n        return this;\n    }\n\n    /**\n     * Append one line.\n     *\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils appendLine() {\n        apply(mTypeCharSequence);\n        mText = LINE_SEPARATOR;\n        return this;\n    }\n\n    /**\n     * Append text and one line.\n     *\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils appendLine(@NonNull final CharSequence text) {\n        apply(mTypeCharSequence);\n        mText = text + LINE_SEPARATOR;\n        return this;\n    }\n\n    /**\n     * Append one image.\n     *\n     * @param bitmap\n     *         The bitmap of image.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils appendImage(@NonNull final Bitmap bitmap) {\n        return appendImage(bitmap, ALIGN_BOTTOM);\n    }\n\n    /**\n     * Append one image.\n     *\n     * @param bitmap\n     *         The bitmap.\n     * @param align\n     *         The alignment.\n     *         <ul>\n     *         <li>{@link Align#ALIGN_TOP     }</li>\n     *         <li>{@link Align#ALIGN_CENTER  }</li>\n     *         <li>{@link Align#ALIGN_BASELINE}</li>\n     *         <li>{@link Align#ALIGN_BOTTOM  }</li>\n     *         </ul>\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils appendImage(@NonNull final Bitmap bitmap, @Align final int align) {\n        apply(mTypeImage);\n        this.imageBitmap = bitmap;\n        this.alignImage = align;\n        return this;\n    }\n\n    /**\n     * Append one image.\n     *\n     * @param drawable\n     *         The drawable of image.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils appendImage(@NonNull final Drawable drawable) {\n        return appendImage(drawable, ALIGN_BOTTOM);\n    }\n\n    /**\n     * Append one image.\n     *\n     * @param drawable\n     *         The drawable of image.\n     * @param align\n     *         The alignment.\n     *         <ul>\n     *         <li>{@link Align#ALIGN_TOP     }</li>\n     *         <li>{@link Align#ALIGN_CENTER  }</li>\n     *         <li>{@link Align#ALIGN_BASELINE}</li>\n     *         <li>{@link Align#ALIGN_BOTTOM  }</li>\n     *         </ul>\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils appendImage(@NonNull final Drawable drawable, @Align final int align) {\n        apply(mTypeImage);\n        this.imageDrawable = drawable;\n        this.alignImage = align;\n        return this;\n    }\n\n    /**\n     * Append one image.\n     *\n     * @param uri\n     *         The uri of image.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils appendImage(@NonNull final Uri uri) {\n        return appendImage(uri, ALIGN_BOTTOM);\n    }\n\n    /**\n     * Append one image.\n     *\n     * @param uri\n     *         The uri of image.\n     * @param align\n     *         The alignment.\n     *         <ul>\n     *         <li>{@link Align#ALIGN_TOP     }</li>\n     *         <li>{@link Align#ALIGN_CENTER  }</li>\n     *         <li>{@link Align#ALIGN_BASELINE}</li>\n     *         <li>{@link Align#ALIGN_BOTTOM  }</li>\n     *         </ul>\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils appendImage(@NonNull final Uri uri, @Align final int align) {\n        apply(mTypeImage);\n        this.imageUri = uri;\n        this.alignImage = align;\n        return this;\n    }\n\n    /**\n     * Append one image.\n     *\n     * @param resourceId\n     *         The resource id of image.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils appendImage(@DrawableRes final int resourceId) {\n        return appendImage(resourceId, ALIGN_BOTTOM);\n    }\n\n    /**\n     * Append one image.\n     *\n     * @param resourceId\n     *         The resource id of image.\n     * @param align\n     *         The alignment.\n     *         <ul>\n     *         <li>{@link Align#ALIGN_TOP     }</li>\n     *         <li>{@link Align#ALIGN_CENTER  }</li>\n     *         <li>{@link Align#ALIGN_BASELINE}</li>\n     *         <li>{@link Align#ALIGN_BOTTOM  }</li>\n     *         </ul>\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils appendImage(@DrawableRes final int resourceId, @Align final int align) {\n        append(Character.toString((char) 0));// it's important for span start with image\n        apply(mTypeImage);\n        this.imageResourceId = resourceId;\n        this.alignImage = align;\n        return this;\n    }\n\n    /**\n     * Append space.\n     *\n     * @param size\n     *         The size of space.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils appendSpace(@IntRange(from = 0) final int size) {\n        return appendSpace(size, Color.TRANSPARENT);\n    }\n\n    /**\n     * Append space.\n     *\n     * @param size\n     *         The size of space.\n     * @param color\n     *         The color of space.\n     * @return the single {@link SpanUtils} instance\n     */\n    public SpanUtils appendSpace(@IntRange(from = 0) final int size, @ColorInt final int color) {\n        apply(mTypeSpace);\n        spaceSize = size;\n        spaceColor = color;\n        return this;\n    }\n\n    private void apply(final int type) {\n        applyLast();\n        mType = type;\n    }\n\n    /**\n     * Create the span string.\n     *\n     * @return the span string\n     */\n    public SpannableStringBuilder create() {\n        applyLast();\n        return mBuilder;\n    }\n\n    private void applyLast() {\n        if (mType == mTypeCharSequence) {\n            updateCharCharSequence();\n        } else if (mType == mTypeImage) {\n            updateImage();\n        } else if (mType == mTypeSpace) {\n            updateSpace();\n        }\n        setDefault();\n    }\n\n    private void updateCharCharSequence() {\n        if (mText.length() == 0) return;\n        int start = mBuilder.length();\n        mBuilder.append(mText);\n        int end = mBuilder.length();\n        if (foregroundColor != COLOR_DEFAULT) {\n            mBuilder.setSpan(new ForegroundColorSpan(foregroundColor), start, end, flag);\n        }\n        if (backgroundColor != COLOR_DEFAULT) {\n            mBuilder.setSpan(new BackgroundColorSpan(backgroundColor), start, end, flag);\n        }\n        if (first != -1) {\n            mBuilder.setSpan(new LeadingMarginSpan.Standard(first, rest), start, end, flag);\n        }\n        if (quoteColor != COLOR_DEFAULT) {\n            mBuilder.setSpan(\n                    new CustomQuoteSpan(quoteColor, stripeWidth, quoteGapWidth),\n                    start,\n                    end,\n                    flag\n            );\n        }\n        if (bulletColor != COLOR_DEFAULT) {\n            mBuilder.setSpan(\n                    new CustomBulletSpan(bulletColor, bulletRadius, bulletGapWidth),\n                    start,\n                    end,\n                    flag\n            );\n        }\n//        if (imGapWidth != -1) {\n//            if (imBitmap != null) {\n//                mBuilder.setSpan(\n//                        new CustomIconMarginSpan(imBitmap, imGapWidth, imAlign),\n//                        start,\n//                        end,\n//                        flag\n//                );\n//            } else if (imDrawable != null) {\n//                mBuilder.setSpan(\n//                        new CustomIconMarginSpan(imDrawable, imGapWidth, imAlign),\n//                        start,\n//                        end,\n//                        flag\n//                );\n//            } else if (imUri != null) {\n//                mBuilder.setSpan(\n//                        new CustomIconMarginSpan(imUri, imGapWidth, imAlign),\n//                        start,\n//                        end,\n//                        flag\n//                );\n//            } else if (imResourceId != -1) {\n//                mBuilder.setSpan(\n//                        new CustomIconMarginSpan(imResourceId, imGapWidth, imAlign),\n//                        start,\n//                        end,\n//                        flag\n//                );\n//            }\n//        }\n        if (fontSize != -1) {\n            mBuilder.setSpan(new AbsoluteSizeSpan(fontSize, fontSizeIsDp), start, end, flag);\n        }\n        if (proportion != -1) {\n            mBuilder.setSpan(new RelativeSizeSpan(proportion), start, end, flag);\n        }\n        if (xProportion != -1) {\n            mBuilder.setSpan(new ScaleXSpan(xProportion), start, end, flag);\n        }\n        if (lineHeight != -1) {\n            mBuilder.setSpan(new CustomLineHeightSpan(lineHeight, alignLine), start, end, flag);\n        }\n        if (isStrikethrough) {\n            mBuilder.setSpan(new StrikethroughSpan(), start, end, flag);\n        }\n        if (isUnderline) {\n            mBuilder.setSpan(new UnderlineSpan(), start, end, flag);\n        }\n        if (isSuperscript) {\n            mBuilder.setSpan(new SuperscriptSpan(), start, end, flag);\n        }\n        if (isSubscript) {\n            mBuilder.setSpan(new SubscriptSpan(), start, end, flag);\n        }\n        if (isBold) {\n            mBuilder.setSpan(new StyleSpan(Typeface.BOLD), start, end, flag);\n        }\n        if (isItalic) {\n            mBuilder.setSpan(new StyleSpan(Typeface.ITALIC), start, end, flag);\n        }\n        if (isBoldItalic) {\n            mBuilder.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), start, end, flag);\n        }\n        if (fontFamily != null) {\n            mBuilder.setSpan(new TypefaceSpan(fontFamily), start, end, flag);\n        }\n        if (typeface != null) {\n            mBuilder.setSpan(new CustomTypefaceSpan(typeface), start, end, flag);\n        }\n        if (alignment != null) {\n            mBuilder.setSpan(new AlignmentSpan.Standard(alignment), start, end, flag);\n        }\n        if (clickSpan != null) {\n            mBuilder.setSpan(clickSpan, start, end, flag);\n        }\n        if (url != null) {\n            mBuilder.setSpan(new URLSpan(url), start, end, flag);\n        }\n        if (blurRadius != -1) {\n            mBuilder.setSpan(\n                    new MaskFilterSpan(new BlurMaskFilter(blurRadius, style)),\n                    start,\n                    end,\n                    flag\n            );\n        }\n        if (shader != null) {\n            mBuilder.setSpan(new ShaderSpan(shader), start, end, flag);\n        }\n        if (shadowRadius != -1) {\n            mBuilder.setSpan(\n                    new ShadowSpan(shadowRadius, shadowDx, shadowDy, shadowColor),\n                    start,\n                    end,\n                    flag\n            );\n        }\n        if (spans != null) {\n            for (Object span : spans) {\n                mBuilder.setSpan(span, start, end, flag);\n            }\n        }\n    }\n\n    private void updateImage() {\n        int start = mBuilder.length();\n        mBuilder.append(\"<img>\");\n        int end = start + 5;\n        if (imageBitmap != null) {\n            mBuilder.setSpan(new CustomImageSpan(imageBitmap, alignImage), start, end, flag);\n        } else if (imageDrawable != null) {\n            mBuilder.setSpan(new CustomImageSpan(imageDrawable, alignImage), start, end, flag);\n        } else if (imageUri != null) {\n            mBuilder.setSpan(new CustomImageSpan(imageUri, alignImage), start, end, flag);\n        } else if (imageResourceId != -1) {\n            mBuilder.setSpan(new CustomImageSpan(imageResourceId, alignImage), start, end, flag);\n        }\n    }\n\n    private void updateSpace() {\n        int start = mBuilder.length();\n        mBuilder.append(\"< >\");\n        int end = start + 3;\n        mBuilder.setSpan(new SpaceSpan(spaceSize, spaceColor), start, end, flag);\n    }\n\n    class CustomLineHeightSpan extends CharacterStyle\n            implements LineHeightSpan {\n\n        private final int height;\n\n        static final int ALIGN_CENTER = 2;\n\n        static final int ALIGN_TOP = 3;\n\n        final int mVerticalAlignment;\n\n        CustomLineHeightSpan(int height, int verticalAlignment) {\n            this.height = height;\n            mVerticalAlignment = verticalAlignment;\n        }\n\n        @Override\n        public void chooseHeight(final CharSequence text, final int start, final int end,\n                                 final int spanstartv, final int v, final Paint.FontMetricsInt fm) {\n            int need = height - (v + fm.descent - fm.ascent - spanstartv);\n//            if (need > 0) {\n            if (mVerticalAlignment == ALIGN_TOP) {\n                fm.descent += need;\n            } else if (mVerticalAlignment == ALIGN_CENTER) {\n                fm.descent += need / 2;\n                fm.ascent -= need / 2;\n            } else {\n                fm.ascent -= need;\n            }\n//            }\n            need = height - (v + fm.bottom - fm.top - spanstartv);\n//            if (need > 0) {\n            if (mVerticalAlignment == ALIGN_TOP) {\n                fm.top += need;\n            } else if (mVerticalAlignment == ALIGN_CENTER) {\n                fm.bottom += need / 2;\n                fm.top -= need / 2;\n            } else {\n                fm.top -= need;\n            }\n//            }\n        }\n\n        @Override\n        public void updateDrawState(final TextPaint tp) {\n\n        }\n    }\n\n    class SpaceSpan extends ReplacementSpan {\n\n        private final int width;\n        private final int color;\n\n        private SpaceSpan(final int width) {\n            this(width, Color.TRANSPARENT);\n        }\n\n        private SpaceSpan(final int width, final int color) {\n            super();\n            this.width = width;\n            this.color = color;\n        }\n\n        @Override\n        public int getSize(@NonNull final Paint paint, final CharSequence text,\n                           @IntRange(from = 0) final int start,\n                           @IntRange(from = 0) final int end,\n                           @Nullable final Paint.FontMetricsInt fm) {\n            return width;\n        }\n\n        @Override\n        public void draw(@NonNull final Canvas canvas, final CharSequence text,\n                         @IntRange(from = 0) final int start,\n                         @IntRange(from = 0) final int end,\n                         final float x, final int top, final int y, final int bottom,\n                         @NonNull final Paint paint) {\n            Paint.Style style = paint.getStyle();\n            int color = paint.getColor();\n\n            paint.setStyle(Paint.Style.FILL);\n            paint.setColor(this.color);\n\n            canvas.drawRect(x, top, x + width, bottom, paint);\n\n            paint.setStyle(style);\n            paint.setColor(color);\n        }\n    }\n\n    class CustomQuoteSpan implements LeadingMarginSpan {\n\n        private final int color;\n        private final int stripeWidth;\n        private final int gapWidth;\n\n        private CustomQuoteSpan(final int color, final int stripeWidth, final int gapWidth) {\n            super();\n            this.color = color;\n            this.stripeWidth = stripeWidth;\n            this.gapWidth = gapWidth;\n        }\n\n        public int getLeadingMargin(final boolean first) {\n            return stripeWidth + gapWidth;\n        }\n\n        public void drawLeadingMargin(final Canvas c, final Paint p, final int x, final int dir,\n                                      final int top, final int baseline, final int bottom,\n                                      final CharSequence text, final int start, final int end,\n                                      final boolean first, final Layout layout) {\n            Paint.Style style = p.getStyle();\n            int color = p.getColor();\n\n            p.setStyle(Paint.Style.FILL);\n            p.setColor(this.color);\n\n            c.drawRect(x, top, x + dir * stripeWidth, bottom, p);\n\n            p.setStyle(style);\n            p.setColor(color);\n        }\n    }\n\n    class CustomBulletSpan implements LeadingMarginSpan {\n\n        private final int color;\n        private final int radius;\n        private final int gapWidth;\n\n        private Path sBulletPath = null;\n\n        private CustomBulletSpan(final int color, final int radius, final int gapWidth) {\n            this.color = color;\n            this.radius = radius;\n            this.gapWidth = gapWidth;\n        }\n\n        public int getLeadingMargin(final boolean first) {\n            return 2 * radius + gapWidth;\n        }\n\n        public void drawLeadingMargin(final Canvas c, final Paint p, final int x, final int dir,\n                                      final int top, final int baseline, final int bottom,\n                                      final CharSequence text, final int start, final int end,\n                                      final boolean first, final Layout l) {\n            if (((Spanned) text).getSpanStart(this) == start) {\n                Paint.Style style = p.getStyle();\n                int oldColor = 0;\n                oldColor = p.getColor();\n                p.setColor(color);\n                p.setStyle(Paint.Style.FILL);\n                if (c.isHardwareAccelerated()) {\n                    if (sBulletPath == null) {\n                        sBulletPath = new Path();\n                        // Bullet is slightly better to avoid aliasing artifacts on mdpi devices.\n                        sBulletPath.addCircle(0.0f, 0.0f, radius, Path.Direction.CW);\n                    }\n                    c.save();\n                    c.translate(x + dir * radius, (top + bottom) / 2.0f);\n                    c.drawPath(sBulletPath, p);\n                    c.restore();\n                } else {\n                    c.drawCircle(x + dir * radius, (top + bottom) / 2.0f, radius, p);\n                }\n                p.setColor(oldColor);\n                p.setStyle(style);\n            }\n        }\n    }\n\n    @SuppressLint(\"ParcelCreator\")\n    class CustomTypefaceSpan extends TypefaceSpan {\n\n        private final Typeface newType;\n\n        private CustomTypefaceSpan(final Typeface type) {\n            super(\"\");\n            newType = type;\n        }\n\n        @Override\n        public void updateDrawState(final TextPaint textPaint) {\n            apply(textPaint, newType);\n        }\n\n        @Override\n        public void updateMeasureState(final TextPaint paint) {\n            apply(paint, newType);\n        }\n\n        private void apply(final Paint paint, final Typeface tf) {\n            int oldStyle;\n            Typeface old = paint.getTypeface();\n            if (old == null) {\n                oldStyle = 0;\n            } else {\n                oldStyle = old.getStyle();\n            }\n\n            int fake = oldStyle & ~tf.getStyle();\n            if ((fake & Typeface.BOLD) != 0) {\n                paint.setFakeBoldText(true);\n            }\n\n            if ((fake & Typeface.ITALIC) != 0) {\n                paint.setTextSkewX(-0.25f);\n            }\n\n            paint.getShader();\n\n            paint.setTypeface(tf);\n        }\n    }\n\n    class CustomImageSpan extends CustomDynamicDrawableSpan {\n        private Drawable mDrawable;\n        private Uri mContentUri;\n        private int mResourceId;\n\n        private CustomImageSpan(final Bitmap b, final int verticalAlignment) {\n            super(verticalAlignment);\n            mDrawable = new BitmapDrawable(mContext.getResources(), b);\n            mDrawable.setBounds(\n                    0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()\n            );\n        }\n\n        private CustomImageSpan(final Drawable d, final int verticalAlignment) {\n            super(verticalAlignment);\n            mDrawable = d;\n            mDrawable.setBounds(\n                    0, 0, mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()\n            );\n        }\n\n        private CustomImageSpan(final Uri uri, final int verticalAlignment) {\n            super(verticalAlignment);\n            mContentUri = uri;\n        }\n\n        private CustomImageSpan(@DrawableRes final int resourceId, final int verticalAlignment) {\n            super(verticalAlignment);\n            mResourceId = resourceId;\n        }\n\n        @Override\n        public Drawable getDrawable() {\n            Drawable drawable = null;\n            if (mDrawable != null) {\n                drawable = mDrawable;\n            } else if (mContentUri != null) {\n                Bitmap bitmap;\n                try {\n                    InputStream is =\n                            mContext.getContentResolver().openInputStream(mContentUri);\n                    bitmap = BitmapFactory.decodeStream(is);\n                    drawable = new BitmapDrawable(mContext.getResources(), bitmap);\n                    drawable.setBounds(\n                            0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()\n                    );\n                    if (is != null) {\n                        is.close();\n                    }\n                } catch (Exception e) {\n                    Log.e(\"sms\", \"Failed to loaded content \" + mContentUri, e);\n                }\n            } else {\n                try {\n                    drawable = ContextCompat.getDrawable(mContext, mResourceId);\n                    drawable.setBounds(\n                            0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()\n                    );\n                } catch (Exception e) {\n                    Log.e(\"sms\", \"Unable to find resource: \" + mResourceId);\n                }\n            }\n            return drawable;\n        }\n    }\n\n    abstract class CustomDynamicDrawableSpan extends ReplacementSpan {\n\n        static final int ALIGN_BOTTOM = 0;\n\n        static final int ALIGN_BASELINE = 1;\n\n        static final int ALIGN_CENTER = 2;\n\n        static final int ALIGN_TOP = 3;\n\n        final int mVerticalAlignment;\n\n        private CustomDynamicDrawableSpan() {\n            mVerticalAlignment = ALIGN_BOTTOM;\n        }\n\n        private CustomDynamicDrawableSpan(final int verticalAlignment) {\n            mVerticalAlignment = verticalAlignment;\n        }\n\n        public abstract Drawable getDrawable();\n\n        @Override\n        public int getSize(@NonNull final Paint paint, final CharSequence text,\n                           final int start, final int end, final Paint.FontMetricsInt fm) {\n            Drawable d = getCachedDrawable();\n            Rect rect = d.getBounds();\n            if (fm != null) {\n//                LogUtils.d(\"fm.top: \" + fm.top,\n//                        \"fm.ascent: \" + fm.ascent,\n//                        \"fm.descent: \" + fm.descent,\n//                        \"fm.bottom: \" + fm.bottom,\n//                        \"lineHeight: \" + (fm.bottom - fm.top));\n                int lineHeight = fm.bottom - fm.top;\n                if (lineHeight < rect.height()) {\n                    if (mVerticalAlignment == ALIGN_TOP) {\n                        fm.top = fm.top;\n                        fm.bottom = rect.height() + fm.top;\n                    } else if (mVerticalAlignment == ALIGN_CENTER) {\n                        fm.top = -rect.height() / 2 - lineHeight / 4;\n                        fm.bottom = rect.height() / 2 - lineHeight / 4;\n                    } else {\n                        fm.top = -rect.height() + fm.bottom;\n                        fm.bottom = fm.bottom;\n                    }\n                    fm.ascent = fm.top;\n                    fm.descent = fm.bottom;\n                }\n            }\n            return rect.right;\n        }\n\n        @Override\n        public void draw(@NonNull final Canvas canvas, final CharSequence text,\n                         final int start, final int end, final float x,\n                         final int top, final int y, final int bottom, @NonNull final Paint paint) {\n            Drawable d = getCachedDrawable();\n            Rect rect = d.getBounds();\n            canvas.save();\n            float transY;\n            int lineHeight = bottom - top;\n//            LogUtils.d(\"rectHeight: \" + rect.height(),\n//                    \"lineHeight: \" + (bottom - top));\n            if (rect.height() < lineHeight) {\n                if (mVerticalAlignment == ALIGN_TOP) {\n                    transY = top;\n                } else if (mVerticalAlignment == ALIGN_CENTER) {\n                    transY = (bottom + top - rect.height()) / 2;\n                } else if (mVerticalAlignment == ALIGN_BASELINE) {\n                    transY = y - rect.height();\n                } else {\n                    transY = bottom - rect.height();\n                }\n                canvas.translate(x, transY);\n            } else {\n                canvas.translate(x, top);\n            }\n            d.draw(canvas);\n            canvas.restore();\n        }\n\n        private Drawable getCachedDrawable() {\n            WeakReference<Drawable> wr = mDrawableRef;\n            Drawable d = null;\n            if (wr != null) {\n                d = wr.get();\n            }\n            if (d == null) {\n                d = getDrawable();\n                mDrawableRef = new WeakReference<>(d);\n            }\n            return d;\n        }\n\n        private WeakReference<Drawable> mDrawableRef;\n    }\n\n    class ShaderSpan extends CharacterStyle implements UpdateAppearance {\n        private Shader mShader;\n\n        private ShaderSpan(final Shader shader) {\n            this.mShader = shader;\n        }\n\n        @Override\n        public void updateDrawState(final TextPaint tp) {\n            tp.setShader(mShader);\n        }\n    }\n\n    class ShadowSpan extends CharacterStyle implements UpdateAppearance {\n        private float radius;\n        private float dx, dy;\n        private int shadowColor;\n\n        private ShadowSpan(final float radius,\n                           final float dx,\n                           final float dy,\n                           final int shadowColor) {\n            this.radius = radius;\n            this.dx = dx;\n            this.dy = dy;\n            this.shadowColor = shadowColor;\n        }\n\n        @Override\n        public void updateDrawState(final TextPaint tp) {\n            tp.setShadowLayer(radius, dx, dy, shadowColor);\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/res/drawable/bg_btn.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <corners android:radius=\"6dp\"/>\n    <solid android:color=\"#e16531\"/>\n</shape>"
  },
  {
    "path": "app/src/main/res/layout/activity_java_demo.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n    <Button\n        android:id=\"@+id/btn_java\"\n        android:text=\"Java使用\"\n        android:layout_marginTop=\"50dp\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"50dp\"/>\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:id=\"@+id/activity_main\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\"\n    android:paddingLeft=\"@dimen/activity_horizontal_margin\"\n    android:paddingTop=\"@dimen/activity_vertical_margin\"\n    android:paddingRight=\"@dimen/activity_horizontal_margin\"\n    android:paddingBottom=\"@dimen/activity_vertical_margin\"\n    tools:context=\"com.example.teprinciple.updateappdemo.MainActivity\">\n\n    <Button\n        android:id=\"@+id/btn_basic_use\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"50dp\"\n        android:text=\"基本使用\"/>\n\n    <Button\n        android:id=\"@+id/btn_download_by_browser\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"20dp\"\n        android:text=\"浏览器下载\"/>\n\n    <Button\n        android:id=\"@+id/btn_higher_level_use\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"20dp\"\n        android:text=\"高级使用\"/>\n\n    <Button\n        android:id=\"@+id/btn_custom_ui\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"20dp\"\n        android:text=\"自定义UI\"/>\n\n\n    <Button\n        android:id=\"@+id/btn_check_md5\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"20dp\"\n        android:text=\"Md5校验\"/>\n\n    <Button\n        android:id=\"@+id/btn_java_sample\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"20dp\"\n        android:text=\"Java\"/>\n\n</LinearLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/check_md5_demo_activity.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"vertical\">\n\n    <TextView\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"20dp\"\n        android:gravity=\"center\"\n        android:text=\"当前应用签名：F5:65:3A:FC:71:C6:AD:EE:BB:B2:D0:D4:67:73:8D:67\"\n        android:textSize=\"12sp\"/>\n\n    <Button\n        android:id=\"@+id/btn_signed\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"50dp\"\n        android:layout_marginTop=\"50dp\"\n        android:text=\"apk正确签名\"/>\n\n    <Button\n        android:id=\"@+id/btn_not_signed\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"50dp\"\n        android:layout_marginTop=\"10dp\"\n        android:text=\"apk错误签名\"/>\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/view_update_dialog_custom.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"280dp\"\n    android:layout_height=\"wrap_content\"\n    android:orientation=\"vertical\">\n\n    <android.support.constraint.ConstraintLayout\n        android:layout_width=\"280dp\"\n        android:layout_height=\"wrap_content\"\n        android:background=\"@drawable/bg_custom_update\"\n        android:paddingBottom=\"15dp\">\n\n        <TextView\n            android:id=\"@+id/tv_update_title\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"130dp\"\n            android:textColor=\"@color/text_title\"\n            android:textSize=\"16sp\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintHorizontal_bias=\"0.502\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            tools:text=\"版本更新啦!\"/>\n\n        <ScrollView\n            android:id=\"@+id/scrollView2\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"90dp\"\n            android:layout_marginTop=\"10dp\"\n            android:overScrollMode=\"never\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/tv_update_title\">\n\n            <TextView\n                android:id=\"@+id/tv_update_content\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_gravity=\"top\"\n                android:gravity=\"start\"\n                android:lineSpacingExtra=\"5dp\"\n                android:paddingLeft=\"20dp\"\n                android:paddingRight=\"20dp\"\n                android:textColor=\"@color/text_content\"\n                android:textSize=\"14sp\"\n                tools:text=\"1、快来升级最新版本\\n2、这次更漂亮了\\n3、快点来吧\"/>\n        </ScrollView>\n\n        <TextView\n            android:id=\"@+id/btn_update_sure\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"35dp\"\n            android:layout_marginStart=\"20dp\"\n            android:layout_marginTop=\"10dp\"\n            android:layout_marginEnd=\"20dp\"\n            android:background=\"@drawable/bg_update_btn\"\n            android:gravity=\"center\"\n            android:text=\"@string/update_now\"\n            android:textColor=\"@color/white\"\n            android:textSize=\"14sp\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintStart_toStartOf=\"parent\"\n            app:layout_constraintTop_toBottomOf=\"@+id/scrollView2\"/>\n\n        <TextView\n            android:id=\"@+id/tv_version_name\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_marginTop=\"40dp\"\n            android:layout_marginEnd=\"15dp\"\n            android:textColor=\"@color/white\"\n            android:textSize=\"14sp\"\n            app:layout_constraintEnd_toEndOf=\"parent\"\n            app:layout_constraintTop_toTopOf=\"parent\"\n            tools:text=\"V1.0.0\"/>\n\n    </android.support.constraint.ConstraintLayout>\n\n    <ImageView\n        android:id=\"@+id/btn_update_cancel\"\n        android:layout_width=\"30dp\"\n        android:layout_height=\"30dp\"\n        android:layout_gravity=\"center_horizontal\"\n        android:layout_marginTop=\"20dp\"\n        android:src=\"@drawable/ic_close\"/>\n\n</LinearLayout>\n\n"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#3F51B5</color>\n    <color name=\"colorPrimaryDark\">#303F9F</color>\n    <color name=\"colorAccent\">#FF4081</color>\n    <color name=\"text_blue\">#0076FF</color>\n    <color name=\"text_black\">#333333</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<resources>\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\">Update</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n\n    <style name=\"CustomDialog\" parent=\"@android:style/Theme.Dialog\">\n        <item name=\"android:windowFrame\">@null</item>\n        <item name=\"android:windowIsFloating\">true</item>\n        <item name=\"android:windowIsTranslucent\">false</item>\n        <item name=\"android:windowNoTitle\">true</item>\n        <item name=\"android:windowBackground\">@drawable/bg_update_dialog</item>\n        <item name=\"android:backgroundDimEnabled\">true</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-w820dp/dimens.xml",
    "content": "<resources>\n    <!-- Example customization of dimensions originally defined in res/values/dimens.update_file_paths\n         (such as screen margins) for screens with more than 820dp of available width. This\n         would include 7\" and 10\" devices in landscape (~960dp and ~1280dp respectively). -->\n    <dimen name=\"activity_horizontal_margin\">64dp</dimen>\n</resources>\n"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    ext.kotlin_version = '1.3.31'\n\n    repositories {\n        jcenter()\n        google()\n        maven { url 'https://jitpack.io' }\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:3.4.1'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n        classpath 'com.novoda:bintray-release:0.9.1'\n    }\n}\n\nallprojects {\n\n    repositories {\n        maven { url 'https://maven.aliyun.com/repository/public/' }\n        maven { url 'http://maven.aliyun.com/nexus/content/repositories/jcenter' }\n        maven { url 'http://maven.aliyun.com/nexus/content/repositories/google' }\n        maven { url 'http://maven.aliyun.com/nexus/content/repositories/gradle-plugin' }\n        jcenter()\n        google()\n        maven { url 'https://jitpack.io' }\n    }\n\n    //中文注释\n    tasks.withType(Javadoc) {\n        options{ encoding \"UTF-8\"\n            charSet 'UTF-8'\n            links \"http://docs.oracle.com/javase/7/docs/api\"\n        }\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n\n\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Mon Jun 03 12:27:34 CST 2019\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-5.1.1-all.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx1536m\n\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto init\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto init\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:init\n@rem Get command-line arguments, handling Windowz variants\n\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\n\n:win9xME_args\n@rem Slurp the command line arguments.\nset CMD_LINE_ARGS=\nset _SKIP=2\n\n:win9xME_args_slurp\nif \"x%~1\" == \"x\" goto execute\n\nset CMD_LINE_ARGS=%*\ngoto execute\n\n:4NT_args\n@rem Get arguments from the 4NT Shell from JP Software\nset CMD_LINE_ARGS=%$\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "readme/README_1.5.2.md",
    "content": "# updateapputils\n\n### 一行代码，快速实现app在线下载更新  A simple library for Android update app\n### 适配Android6.0、7.0、8.0\n![](update.gif)\n\n## 集成\ncompile引入\n```\ndependencies {\n    implementation 'com.teprinciple:updateapputils:1.5.2'\n}\n```\n\n## 使用\n更新检测一般放在MainActivity或者启动页上，\n在请求服务器版本检测接口获取到versionCode、versionName、最新apkPath后调用。\n\n#### 快速使用\n```\n UpdateAppUtils.from(this)\n                .serverVersionCode(2)  //服务器versionCode\n                .serverVersionName(\"2.0\") //服务器versionName\n                .apkPath(apkPath) //最新apk下载地址\n                .update();\n```\n#### Kotlin代码调用完全一样\n```\n   private fun update() {\n        val apkPath:String = \"http://issuecdn.baidupcs.com/issue/netdisk/apk/BaiduNetdisk_7.15.1.apk\"\n\n        UpdateAppUtils.from(this)\n                .serverVersionCode(2)\n                .serverVersionName(\"2.0\")\n                .apkPath(apkPath)\n                .update()\n    }\n\n```\n\n#### 更多配置使用\n```\nUpdateAppUtils.from(this)\n                .checkBy(UpdateAppUtils.CHECK_BY_VERSION_NAME) //更新检测方式，默认为VersionCode\n                .serverVersionCode(2)\n                .serverVersionName(\"2.0\")\n                .apkPath(apkPath)\n                .showNotification(false) //是否显示下载进度到通知栏，默认为true\n                .updateInfo(info)  //更新日志信息 String\n                .downloadBy(UpdateAppUtils.DOWNLOAD_BY_BROWSER) //下载方式：app下载、手机浏览器下载。默认app下载\n                .isForce(true) //是否强制更新，默认false 强制更新情况下用户不同意更新则不能使用app\n                .update();\n```\n\n#### 说明\n```\n    1、UpdateAppUtils提供两种更新判断方式\n\n    CHECK_BY_VERSION_CODE:通过versionCode判断，服务器上versionCode > 本地versionCode则执行更新\n\n    CHECK_BY_VERSION_NAME：通过versionName判断，服务器上versionName 与 本地versionName不同则更新\n\n    2、UpdateAppUtils提供两种下载apk方式\n\n    DOWNLOAD_BY_APP：通过App下载\n\n    DOWNLOAD_BY_BROWSER：通过手机浏览器下载\n\n```\n\n#### 关于适配Android6.0、7.0、8.0\n\n库内部已经完全适配至8.0,你可以不用再对该库进行适配\n\n#### 文章地址：[《UpdateAppUtils一行代码实现app在线更新》](http://www.jianshu.com/p/9c91bb984c85)\n\n#### 更新日志\n1.5.2<br>\n修复部分bug\n<br>1.5.1<br>\n库内部适配至Android8.0\n<br>1.4<br>\n使用[filedownloader](https://github.com/lingochamp/FileDownloader)替换DownloadManager，避免部分手机DownLoadManager无效，同时解决了重复下载的问题，且提高了下载速度\n增加接口UpdateAppUtils.needFitAndroidN(false)，避免不需要适配7.0，也要设置FileProvider\n<br>1.3.1<br>\n修复部分bug，在demo中加入kotlin调用代码\n<br>1.3<br>\n增加接口方法 showNotification(false)//是否显示下载进度到通知栏；<br>updateInfo(info)//更新日志信息；下载前WiFi判断。\n<br>1.2<br>\n适配Android7.0，并在demo中加入适配6.0和7.0的代码\n<br>1.1<br>\n适配更多SdkVersion"
  },
  {
    "path": "readme/version.md",
    "content": "### 更新日志\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* 增加'暂不更新'按钮点击监听 setCancelBtnClickListener()\n* 增加'立即更新'按钮点击监听 setUpdateBtnClickListener()\n* 修复部分bug\n#### 2.0.4\n* 修复阿里云，码云平台上的apk FileDownloader下载失败\n* 增加UpdateConfig alwaysShowDownLoadDialog字段，让非强更也能显示下载进度弹窗\n#### 2.0.3\n* 更新弹窗内容支持SpannableString\n#### 2.0.2\n* 9.0Http适配\n#### 2.0.1\n* 自定义FileProvide，防止provider冲突\n#### 2.0.0\n* Kotlin重构\n* 支持AndroidX\n* 安装包签名文件md5校验\n* 通知栏自定义图标\n* 支持自定义UI\n* 适配中英文\n* 增加下载回调等api\n* 修复部分bug\n#### 1.5.2\n* 修复部分bug\n#### 1.5.1\n* 库内部适配至Android8.0\n#### 1.4\n* 使用[filedownloader](https://github.com/lingochamp/FileDownloader)替换DownloadManager，避免部分手机DownLoadManager无效，同时解决了重复下载的问题，且提高了下载速度\n* 增加接口UpdateAppUtils.needFitAndroidN(false)，避免不需要适配7.0，也要设置FileProvider\n#### 1.3.1\n* 修复部分bug，在demo中加入kotlin调用代码\n#### 1.3\n* 增加接口方法 showNotification(false) //是否显示下载进度到通知栏；\n* updateInfo(info) //更新日志信息；\n* 下载前WiFi判断。\n#### 1.2\n* 适配Android7.0，并在demo中加入适配6.0和7.0的代码\n#### 1.1\n* 适配更多SdkVersion"
  },
  {
    "path": "readme/自定义UI.md",
    "content": "## 完全自定义UI\n\n### 1、创建你的layout（必须）\n你可以创建任意你想要的UI布局（[参考 view_update_dialog_custom.xml](https://github.com/teprinciple/UpdateAppUtils/blob/master/app/src/main/res/layout/view_update_dialog_custom.xml)）\n，但是控件id需要保持如下：\n\n| id                  | 说明                 |      控件类型        | 是否必须 |\n|:--------------------- |:-------------------|:----------------- |:------ |\n| btn_update_sure       | 立即更新按钮id| 任意View |true |\n| btn_update_cancel     | 暂不更新按钮id| 任意View  |true  |\n| tv_update_title     | 更新弹窗标题| TextView |false  |\n| tv_update_content     | 更新内容| TextView  |false  |\n\nbtn_update_sure和btn_update_cancel是必须提供的，否则更新无法继续；\n\ntv_update_title，tv_update_content提供，UpdateAppUtils内部会自动\n设置值，如果你不想这样，也可以自行命名，稍后通过OnInitUiListener接口进行相关文案设置；\n\n### 2、注入到UpdateAppUtils（必须）\n\n通过设置uiConfig，将自定义布局注入到UpdateAppUtils；注意uiType必须为UiType.CUSTOM\n\n```\nUpdateAppUtils\n    .getInstance()\n    // ...\n    .uiConfig(UiConfig(uiType = UiType.CUSTOM, customLayoutId = R.layout.view_update_dialog_custom))\n    .update()\n```\n\n### 3、实现OnInitUiListener接口（非必须）\n\nUpdateAppUtils 中只对上表中的4个控件进行了相关内容的填充，如果你自定义的布局中有其他控件需要进行内容填充\n需要实现OnInitUiListener接口来进行操作：\n```\nUpdateAppUtils\n    .getInstance()\n    // ...\n    .setOnInitUiListener(object : OnInitUiListener {\n        override fun onInitUpdateUi(view: View?, updateConfig: UpdateConfig, uiConfig: UiConfig) {\n            view?.findViewById<TextView>(R.id.tv_update_title)?.text = \"版本更新啦\"\n            view?.findViewById<TextView>(R.id.tv_version_name)?.text = \"V2.0.0\"\n            // do more...\n        }\n    })\n```"
  },
  {
    "path": "settings.gradle",
    "content": "include ':app', ':updateapputils'\n"
  },
  {
    "path": "updateapputils/.gitignore",
    "content": "/build\n"
  },
  {
    "path": "updateapputils/build.gradle",
    "content": "apply plugin: 'com.android.library'\napply plugin: 'kotlin-android-extensions'\napply plugin: 'kotlin-android'\napply plugin: 'com.novoda.bintray-release'\n\nandroid {\n    compileSdkVersion 29\n    defaultConfig {\n        minSdkVersion 19\n        targetSdkVersion 29\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n\n    // 忽略错误信息\n    lintOptions {\n        abortOnError false\n    }\n\n    androidExtensions {\n        experimental = true\n    }\n}\n\ndependencies {\n    implementation fileTree(include: ['*.jar'], dir: 'libs')\n    implementation 'com.android.support:appcompat-v7:28.0.0'\n    implementation 'com.android.support.constraint:constraint-layout:1.1.3'\n    implementation 'com.liulishuo.filedownloader:library:1.7.7'\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1'\n}\n\nrepositories {\n    mavenCentral()\n}\n\npublish {\n    userOrg = 'teprinciple'\n    groupId = 'com.teprinciple'\n    artifactId = 'updateapputils'\n    publishVersion = '2.3.0'\n    desc = 'A Simple library for Android update app'\n    website = 'https://github.com/teprinciple/UpdateAppUtils'\n}"
  },
  {
    "path": "updateapputils/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in /Users/teprinciple/Library/Android/sdk/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the proguardFiles\n# directive in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "updateapputils/src/androidTest/java/teprinciple/library/ExampleInstrumentedTest.java",
    "content": "package teprinciple.library;\n\nimport android.content.Context;\nimport android.support.test.InstrumentationRegistry;\nimport android.support.test.runner.AndroidJUnit4;\n\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\nimport static org.junit.Assert.*;\n\n/**\n * Instrumentation test, which will execute on an Android device.\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\n@RunWith(AndroidJUnit4.class)\npublic class ExampleInstrumentedTest {\n    @Test\n    public void useAppContext() throws Exception {\n        // Context of the app under test.\n        Context appContext = InstrumentationRegistry.getTargetContext();\n\n        assertEquals(\"teprinciple.library.test\", appContext.getPackageName());\n    }\n}\n"
  },
  {
    "path": "updateapputils/src/main/AndroidManifest.xml",
    "content": "<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n          xmlns:tools=\"http://schemas.android.com/tools\"\n          package=\"com.teprinciple.updateapputils\">\n\n    <uses-permission android:name=\"android.permission.DOWNLOAD_WITHOUT_NOTIFICATION\"/>\n    <uses-permission android:name=\"android.permission.INTERNET\"/>\n    <uses-permission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"/>\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>\n\n    <uses-permission android:name=\"android.permission.REQUEST_INSTALL_PACKAGES\"/>\n\n    <uses-permission android:name=\"android.permission.KILL_BACKGROUND_PROCESSES\"/>\n\n    <uses-permission android:name=\"android.permission.RESTART_PACKAGES\"/>\n\n    <application\n        android:networkSecurityConfig=\"@xml/network_security_config\"\n        tools:ignore=\"UnusedAttribute\">\n\n        <activity\n            android:name=\"ui.UpdateAppActivity\"\n            android:launchMode=\"singleTask\"\n            android:theme=\"@style/DialogActivityTheme\"/>\n\n        <service android:name=\"update.UpdateAppService\"/>\n\n        <provider\n            android:name=\"update.UpdateFileProvider\"\n            android:authorities=\"${applicationId}.fileprovider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/update_file_paths\"/>\n        </provider>\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "updateapputils/src/main/java/constant/DownLoadBy.kt",
    "content": "package constant\n\n/**\n * desc: 下载方式\n * time: 2019/6/18\n * @author yk\n */\nobject DownLoadBy {\n    /**\n     * app下载\n     */\n    const val APP = 0x101\n\n    /**\n     * 浏览器下载\n     */\n    const val BROWSER = 0x102\n}"
  },
  {
    "path": "updateapputils/src/main/java/constant/UiType.kt",
    "content": "package constant\n\n/**\n * desc: UI 类型\n * time: 2019/6/27\n * @author yk\n */\nobject UiType {\n\n    /**\n     * 简洁版\n     */\n    const val SIMPLE = \"SIMPLE\"\n\n    /**\n     * 丰富版\n     */\n    const val PLENTIFUL = \"PLENTIFUL\"\n\n    /**\n     * 全自定义\n     */\n    const val CUSTOM = \"CUSTOM\"\n}"
  },
  {
    "path": "updateapputils/src/main/java/extension/BooleanKtx.kt",
    "content": "package extension\n\nimport kotlin.contracts.ExperimentalContracts\nimport kotlin.contracts.InvocationKind\nimport kotlin.contracts.contract\n\n@UseExperimental(ExperimentalContracts::class)\ninline fun Boolean?.yes(block: () -> Unit): Boolean? {\n    contract {\n        callsInPlace(block, InvocationKind.AT_MOST_ONCE)\n    }\n    if (this == true) block()\n    return this\n}\n\n@UseExperimental(ExperimentalContracts::class)\ninline fun Boolean?.no(block: () -> Unit): Boolean? {\n    contract {\n        callsInPlace(block, InvocationKind.AT_MOST_ONCE)\n    }\n    if (this != true) block()\n    return this\n}"
  },
  {
    "path": "updateapputils/src/main/java/extension/ContextKtx.kt",
    "content": "package extension\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.ConnectivityManager\nimport android.net.Uri\nimport android.os.Build\nimport android.support.v4.content.FileProvider\nimport java.io.File\n\n/**\n * desc: context 相关扩展\n * author: teprinciple on 2020/3/27.\n */\n\n\n/**\n * appName\n */\nval Context.appName\n    get() = packageManager.getPackageInfo(packageName, 0)?.applicationInfo?.loadLabel(packageManager).toString()\n\n/**\n * 检测wifi是否连接\n */\nfun Context.isWifiConnected(): Boolean {\n    val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager\n    cm ?: return false\n    val networkInfo = cm.activeNetworkInfo\n    return networkInfo != null && networkInfo.type == ConnectivityManager.TYPE_WIFI\n}\n\n\n/**\n * 跳转安装\n */\nfun Context.installApk(apkPath: String?) {\n\n    if (apkPath.isNullOrEmpty())return\n\n    val intent = Intent(Intent.ACTION_VIEW)\n    val apkFile = File(apkPath)\n\n    // android 7.0 fileprovider 适配\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n        val contentUri = FileProvider.getUriForFile(this, this.packageName + \".fileprovider\", apkFile)\n        intent.setDataAndType(contentUri, \"application/vnd.android.package-archive\")\n    } else {\n        intent.setDataAndType(Uri.fromFile(apkFile), \"application/vnd.android.package-archive\")\n    }\n\n    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n    this.startActivity(intent)\n}\n\n\n"
  },
  {
    "path": "updateapputils/src/main/java/extension/CoreKtx.kt",
    "content": "package extension\n\nimport android.app.ActivityManager\nimport android.content.Context\nimport android.os.Build\nimport android.support.v4.content.ContextCompat\nimport android.util.Log\nimport android.view.View\nimport update.UpdateAppUtils\nimport util.GlobalContextProvider\nimport kotlin.system.exitProcess\n\n/**\n * desc: 扩展\n * author: teprinc\n * iple on 2020/3/27.\n */\n\n/**\n * 全局context\n */\nfun globalContext() = GlobalContextProvider.mContext\n\n\n/**\n * 打印日志\n */\nfun log(content: String?) = UpdateAppUtils.updateInfo.config.isDebug.yes {\n    Log.e(\"[UpdateAppUtils]\", content ?: \"\")\n}\n\n/**\n * 获取color\n */\nfun color(color: Int) = if (globalContext() == null) 0 else ContextCompat.getColor(globalContext()!!, color)\n\n/**\n * 获取 String\n */\nfun string(string: Int) = globalContext()?.getString(string) ?: \"\"\n\n/**\n * view 显示隐藏\n */\nfun View.visibleOrGone(show: Boolean) {\n    if (show) {\n        this.visibility = View.VISIBLE\n    } else {\n        this.visibility = View.GONE\n    }\n}\n\n/**\n * 退出app\n */\nfun exitApp() {\n    val manager = globalContext()!!.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n        manager.appTasks.forEach { it.finishAndRemoveTask() }\n    } else {\n        exitProcess(0)\n    }\n}"
  },
  {
    "path": "updateapputils/src/main/java/extension/StringKtx.kt",
    "content": "package extension\n\nimport java.io.File\n\n/**\n * desc: string 相关扩展\n * author: teprinciple on 2020/3/27.\n */\n\n/**\n * 根据文件路径删除文件\n */\nfun String?.deleteFile() {\n    kotlin.runCatching {\n        val file = File(this ?: \"\")\n        (file.isFile).yes {\n            file.delete()\n            log(\"删除成功\")\n        }\n    }.onFailure {\n        log(it.message)\n    }\n}"
  },
  {
    "path": "updateapputils/src/main/java/listener/Md5CheckResultListener.kt",
    "content": "package listener\n\n/**\n * desc: Md5校验结果回调\n * time: 2019/6/21\n * @author teprinciple\n */\ninterface Md5CheckResultListener {\n    fun onResult(result: Boolean)\n}"
  },
  {
    "path": "updateapputils/src/main/java/listener/OnBtnClickListener.kt",
    "content": "package listener\n\n/**\n * desc: 按钮点击监听\n * time: 2019/9/16\n * @author teprinciple\n */\ninterface OnBtnClickListener {\n\n    /**\n     * 按钮点击\n     * @return 是否消费事件\n     */\n    fun onClick(): Boolean\n}"
  },
  {
    "path": "updateapputils/src/main/java/listener/OnInitUiListener.kt",
    "content": "package listener\n\nimport android.view.View\nimport model.UiConfig\nimport model.UpdateConfig\n\n/**\n * desc: 初始化UI 回调 用于进一步自定义UI\n * time: 2019/6/28\n * @author teprinciple\n */\ninterface OnInitUiListener {\n\n    /**\n     * 初始化更新弹窗回调\n     * @param view 弹窗view\n     * @param updateConfig 当前更新配置\n     * @param uiConfig 当前ui配置\n     */\n    fun onInitUpdateUi(view: View?, updateConfig: UpdateConfig, uiConfig: UiConfig)\n}"
  },
  {
    "path": "updateapputils/src/main/java/listener/UpdateDownloadListener.kt",
    "content": "package listener\n\n/**\n * desc: 下载监听\n * time: 2019/6/19\n * @author teprinciple\n */\ninterface UpdateDownloadListener {\n\n    /**\n     * 开始下载\n     */\n    fun onStart()\n\n    /**\n     * 下载中\n     * @param progress 进度 0 - 100\n     */\n    fun onDownload(progress: Int)\n\n    /**\n     * 下载完成\n     */\n    fun onFinish()\n\n    /**\n     * 下载错误\n     */\n    fun onError(e: Throwable)\n}"
  },
  {
    "path": "updateapputils/src/main/java/model/UiConfig.kt",
    "content": "package model\n\nimport com.teprinciple.updateapputils.R\nimport constant.UiType\nimport extension.string\n\n/**\n * desc: UiConfig UI 配置\n * time: 2019/6/27\n * @author teprinciple\n */\ndata class UiConfig(\n    // ui类型，默认简洁版\n    var uiType: String = UiType.SIMPLE,\n    // 自定义UI 布局id\n    var customLayoutId: Int? = null,\n    // 更新弹窗中的logo\n    var updateLogoImgRes: Int? = null,\n    // 标题相关设置\n    var titleTextSize: Float? = null,\n    var titleTextColor: Int? = null,\n    // 更新内容相关设置\n    var contentTextSize: Float? = null,\n    var contentTextColor: Int? = null,\n    // 更新按钮相关设置\n    var updateBtnBgColor: Int? = null,\n    var updateBtnBgRes: Int? = null,\n    var updateBtnTextColor: Int? = null,\n    var updateBtnTextSize: Float? = null,\n    var updateBtnText: CharSequence = string(R.string.update_now),\n    // 取消按钮相关设置\n    var cancelBtnBgColor: Int? = null,\n    var cancelBtnBgRes: Int? = null,\n    var cancelBtnTextColor: Int? = null,\n    var cancelBtnTextSize: Float? = null,\n    var cancelBtnText: CharSequence = string(R.string.update_cancel),\n\n    // 开始下载时的Toast提示文字\n    var downloadingToastText: CharSequence = string(R.string.toast_download_apk),\n    // 下载中 下载按钮以及通知栏标题前缀，进度自动拼接在后面\n    var downloadingBtnText: CharSequence = string(R.string.downloading),\n    // 下载出错时，下载按钮及通知栏标题\n    var downloadFailText: CharSequence = string(R.string.download_fail)\n)"
  },
  {
    "path": "updateapputils/src/main/java/model/UpdateConfig.kt",
    "content": "package model\n\nimport constant.DownLoadBy\n\ndata class UpdateConfig(\n    var isDebug: Boolean = true, // 是否是调试模式，调试模式会输出日志\n\n    var alwaysShow: Boolean = true, // 非强制更新时，是否每次都显示弹窗，用VersionName来判断\n    var thisTimeShow: Boolean = false, // 非强制更新时，指定本次显示弹窗\n    var alwaysShowDownLoadDialog: Boolean = false, // 非强制更新时，也显示下载进度dialog\n    var force: Boolean = false, // 是否强制更新\n    var apkSavePath: String = \"\", // apk下载存放位置\n    var apkSaveName: String = \"\", // apk 保存名（默认是app的名字）\n    var downloadBy: Int = DownLoadBy.APP, // 下载方式：默认app下载\n    //var downloadDirect: Boolean = false, // 不需要弹窗，直接开始下载安装\n    var checkWifi: Boolean = false, // 是否检查是否wifi\n    var isShowNotification: Boolean = true, // 是否在通知栏显示\n    var notifyImgRes: Int = 0, // 通知栏图标\n    var needCheckMd5: Boolean = false, // 是否需要进行md5校验，仅app下载方式有效\n    var showDownloadingToast: Boolean = true, // 是否需要显示 【更新下载中】文案\n    var serverVersionName: String = \"\", // 服务器上版本名\n    var serverVersionCode: Int = 0 // 服务器上版本号\n)"
  },
  {
    "path": "updateapputils/src/main/java/model/UpdateInfo.kt",
    "content": "package model\n\nimport com.teprinciple.updateapputils.R\nimport extension.string\n\n/**\n * desc: UpdateInfo\n * time: 2019/6/18\n * @author teprinciple\n */\ninternal data class UpdateInfo(\n    // 更新标题\n    var updateTitle: CharSequence = string(R.string.update_title),\n    // 更新内容\n    var updateContent: CharSequence = string(R.string.update_content),\n    // apk 下载地址\n    var apkUrl: String = \"\",\n    // 更新配置\n    var config: UpdateConfig = UpdateConfig(),\n    // ui配置\n    var uiConfig: UiConfig = UiConfig()\n)"
  },
  {
    "path": "updateapputils/src/main/java/ui/UpdateAppActivity.kt",
    "content": "package ui\n\nimport android.Manifest\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.content.pm.PackageManager\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Bundle\nimport android.provider.Settings\nimport android.support.v4.app.ActivityCompat\nimport android.support.v4.content.ContextCompat\nimport android.support.v7.app.AppCompatActivity\nimport android.view.MotionEvent\nimport android.view.View\nimport android.widget.ImageView\nimport android.widget.TextView\nimport android.widget.Toast\nimport com.teprinciple.updateapputils.R\nimport constant.DownLoadBy\nimport constant.UiType\nimport extension.*\nimport update.DownloadAppUtils\nimport update.UpdateAppService\nimport update.UpdateAppUtils\nimport util.AlertDialogUtil\nimport util.GlobalContextProvider\nimport util.SPUtil\n\n/**\n * desc: 更新弹窗\n * author: teprinciple on 2019/06/3.\n */\ninternal class UpdateAppActivity : AppCompatActivity() {\n\n    private var tvTitle: TextView? = null\n    private var tvContent: TextView? = null\n    private var sureBtn: View? = null\n    private var cancelBtn: View? = null\n    private var ivLogo: ImageView? = null\n\n    /**\n     * 更新信息\n     */\n    private val updateInfo by lazy { UpdateAppUtils.updateInfo }\n\n    /**\n     * 更新配置\n     */\n    private val updateConfig by lazy { updateInfo.config }\n\n    /**\n     * ui 配置\n     */\n    private val uiConfig by lazy { updateInfo.uiConfig }\n\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        if (GlobalContextProvider.mContext == null){\n            GlobalContextProvider.mContext = this.applicationContext\n        }\n\n        setContentView(\n            when (uiConfig.uiType) {\n                UiType.SIMPLE -> R.layout.view_update_dialog_simple\n                UiType.PLENTIFUL -> R.layout.view_update_dialog_plentiful\n                UiType.CUSTOM -> uiConfig.customLayoutId ?: R.layout.view_update_dialog_simple\n                else -> R.layout.view_update_dialog_simple\n            }\n        )\n        initView()\n        initUi()\n\n        // 初始化UI回调，用于进一步自定义UI\n        UpdateAppUtils.onInitUiListener?.onInitUpdateUi(\n            window.decorView.findViewById(android.R.id.content),\n            updateConfig,\n            uiConfig)\n\n        // 每次弹窗后，下载前均把本地之前缓存的apk删除，避免缓存老版本apk或者问题apk，并不重新下载新的apk\n        SPUtil.getString(DownloadAppUtils.KEY_OF_SP_APK_PATH, \"\").deleteFile()\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    private fun initView() {\n\n        tvTitle = findViewById(R.id.tv_update_title)\n        tvContent = findViewById(R.id.tv_update_content)\n        cancelBtn = findViewById(R.id.btn_update_cancel)\n        sureBtn = findViewById(R.id.btn_update_sure)\n        ivLogo = findViewById(R.id.iv_update_logo)\n\n        // 更新标题\n        tvTitle?.text = updateInfo.updateTitle\n\n        // 更新内容\n        tvContent?.text = updateInfo.updateContent\n\n        // 取消\n        cancelBtn?.setOnClickListener {\n            updateConfig.force.yes {\n                exitApp()\n            }.no {\n                finish()\n            }\n        }\n\n        // 确定\n        sureBtn?.setOnClickListener {\n\n            DownloadAppUtils.isDownloading.no {\n                if (sureBtn is TextView) {\n                    (sureBtn as? TextView)?.text = uiConfig.updateBtnText\n                }\n                preDownLoad()\n            }\n        }\n\n        // 显示或隐藏取消按钮, 强更时默认不显示取消按钮\n        hideShowCancelBtn(!updateConfig.force)\n\n        // 外部额外设置 取消 按钮点击事件\n        cancelBtn?.setOnTouchListener { v, event ->\n            when (event.action) {\n                MotionEvent.ACTION_UP -> {\n                    UpdateAppUtils.onCancelBtnClickListener?.onClick() ?: false\n                }\n                else -> false\n            }\n        }\n\n        // 外部额外设置 立即更新 按钮点击事件\n        sureBtn?.setOnTouchListener { v, event ->\n            when (event.action) {\n                MotionEvent.ACTION_UP -> {\n                    UpdateAppUtils.onUpdateBtnClickListener?.onClick() ?: false\n                }\n                else -> false\n            }\n        }\n    }\n\n    /**\n     * 取消按钮处理\n     */\n    private fun hideShowCancelBtn(show: Boolean) {\n        // 强制更新 不显示取消按钮\n        cancelBtn?.visibleOrGone(show)\n        // 取消按钮与确定按钮中的间隔线\n        findViewById<View>(R.id.view_line)?.visibleOrGone(show)\n    }\n\n    /**\n     * 初始化UI\n     */\n    private fun initUi() {\n\n        uiConfig.apply {\n            // 设置更新logo\n            updateLogoImgRes?.let { ivLogo?.setImageResource(it) }\n            // 设置标题字体颜色、大小\n            titleTextColor?.let { tvTitle?.setTextColor(it) }\n            titleTextSize?.let { tvTitle?.setTextSize(it) }\n            // 设置标题字体颜色、大小\n            contentTextColor?.let { tvContent?.setTextColor(it) }\n            contentTextSize?.let { tvContent?.setTextSize(it) }\n            // 更新按钮相关设置\n            updateBtnBgColor?.let { sureBtn?.setBackgroundColor(it) }\n            updateBtnBgRes?.let { sureBtn?.setBackgroundResource(it) }\n            if (sureBtn is TextView) {\n                updateBtnTextColor?.let { (sureBtn as? TextView)?.setTextColor(it) }\n                updateBtnTextSize?.let { (sureBtn as? TextView)?.setTextSize(it) }\n                (sureBtn as? TextView)?.text = updateBtnText\n            }\n\n            // 取消按钮相关设置\n            cancelBtnBgColor?.let { cancelBtn?.setBackgroundColor(it) }\n            cancelBtnBgRes?.let { cancelBtn?.setBackgroundResource(it) }\n            if (cancelBtn is TextView) {\n                cancelBtnTextColor?.let { (cancelBtn as? TextView)?.setTextColor(it) }\n                cancelBtnTextSize?.let { (cancelBtn as? TextView)?.setTextSize(it) }\n                (cancelBtn as? TextView)?.text = cancelBtnText\n            }\n        }\n    }\n\n    override fun onBackPressed() {\n        // do noting 禁用返回键\n    }\n\n    /**\n     * 预备下载 进行 6.0权限检查\n     */\n    private fun preDownLoad() {\n        // 6.0 以下不用动态权限申请\n        (Build.VERSION.SDK_INT < Build.VERSION_CODES.M ).yes {\n            download()\n        }.no {\n            val writePermission = ContextCompat.checkSelfPermission(this, permission)\n            (writePermission == PackageManager.PERMISSION_GRANTED).yes {\n                download()\n            }.no {\n                // 申请权限\n                ActivityCompat.requestPermissions(this, arrayOf(permission), PERMISSION_CODE)\n            }\n        }\n    }\n\n    /**\n     * 下载判断\n     */\n    private fun download() {\n        // 动态注册广播，8.0 静态注册收不到\n        // 开启服务注册，避免直接在Activity中注册广播生命周期随Activity终止而终止\n        startService(Intent(this, UpdateAppService::class.java))\n\n        when (updateConfig.downloadBy) {\n            // App下载\n            DownLoadBy.APP -> {\n                (updateConfig.checkWifi && !isWifiConnected()).yes {\n                    // 需要进行WiFi判断\n                    AlertDialogUtil.show(this, getString(R.string.check_wifi_notice), onSureClick = {\n                        realDownload()\n                    })\n                }.no {\n                    // 不需要wifi判断，直接下载\n                    realDownload()\n                }\n            }\n\n            // 浏览器下载\n            DownLoadBy.BROWSER -> {\n                DownloadAppUtils.downloadForWebView(updateInfo.apkUrl)\n            }\n        }\n    }\n\n    /**\n     * 实际下载\n     */\n    @SuppressLint(\"SetTextI18n\")\n    private fun realDownload() {\n\n        if ((updateConfig.force || updateConfig.alwaysShowDownLoadDialog) && sureBtn is TextView) {\n            DownloadAppUtils.onError = {\n                (sureBtn as? TextView)?.text = uiConfig.downloadFailText\n                (updateConfig.alwaysShowDownLoadDialog).yes {\n                    hideShowCancelBtn(true)\n                }\n            }\n\n            DownloadAppUtils.onReDownload = {\n                (sureBtn as? TextView)?.text = uiConfig.updateBtnText\n            }\n\n            DownloadAppUtils.onProgress = {\n                (it == 100).yes {\n                    (sureBtn as? TextView)?.text = getString(R.string.install)\n                    (updateConfig.alwaysShowDownLoadDialog).yes {\n                        hideShowCancelBtn(true)\n                    }\n                }.no {\n                    (sureBtn as? TextView)?.text = \"${uiConfig.downloadingBtnText}$it%\"\n                    (updateConfig.alwaysShowDownLoadDialog).yes {\n                        hideShowCancelBtn(false)\n                    }\n                }\n            }\n        }\n\n        DownloadAppUtils.download()\n\n        (updateConfig.showDownloadingToast).yes {\n            Toast.makeText(this, uiConfig.downloadingToastText, Toast.LENGTH_SHORT).show()\n        }\n\n        // 非强制安装且alwaysShowDownLoadDialog为false时，开始下载后取消弹窗\n        (!updateConfig.force && !updateConfig.alwaysShowDownLoadDialog).yes {\n            finish()\n        }\n    }\n\n    /**\n     * 权限请求结果\n     */\n    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults)\n\n        when (requestCode) {\n            PERMISSION_CODE -> (grantResults.getOrNull(0) == PackageManager.PERMISSION_GRANTED).yes {\n                download()\n            }.no {\n                ActivityCompat.shouldShowRequestPermissionRationale(this, permission).no {\n                    // 显示无权限弹窗\n                    AlertDialogUtil.show(this, getString(R.string.no_storage_permission), onSureClick = {\n                        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)\n                        intent.data = Uri.parse(\"package:$packageName\") // 根据包名打开对应的设置界面\n                        startActivity(intent)\n                    })\n                }\n            }\n        }\n    }\n\n    override fun finish() {\n        super.finish()\n        overridePendingTransition(0, 0)\n    }\n\n    companion object {\n\n        fun launch() = globalContext()?.let {\n            val intent = Intent(it, UpdateAppActivity::class.java)\n            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK\n            it.startActivity(intent)\n        }\n\n        private const val permission = Manifest.permission.WRITE_EXTERNAL_STORAGE\n\n        private const val PERMISSION_CODE = 1001\n    }\n}\n"
  },
  {
    "path": "updateapputils/src/main/java/update/DownloadAppUtils.kt",
    "content": "package update\n\nimport android.content.Context\nimport android.content.Intent\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Environment\nimport com.liulishuo.filedownloader.BaseDownloadTask\nimport com.liulishuo.filedownloader.FileDownloadLargeFileListener\nimport com.liulishuo.filedownloader.FileDownloader\nimport extension.*\nimport util.FileDownloadUtil\nimport util.SPUtil\nimport util.SignMd5Util\nimport java.io.File\n\n/**\n * Created by Teprinciple on 2016/12/13.\n */\ninternal object DownloadAppUtils {\n\n    const val KEY_OF_SP_APK_PATH = \"KEY_OF_SP_APK_PATH\"\n\n    /**\n     * apk 下载后本地文件路径\n     */\n    var downloadUpdateApkFilePath: String = \"\"\n\n    /**\n     * 更新信息\n     */\n    private val updateInfo by lazy { UpdateAppUtils.updateInfo }\n\n    /**\n     * context\n     */\n    private val context by lazy { globalContext()!! }\n\n    /**\n     * 是否在下载中\n     */\n    var isDownloading = false\n\n    /**\n     *下载进度回调\n     */\n    var onProgress: (Int) -> Unit = {}\n\n    /**\n     * 下载出错回调\n     */\n    var onError: () -> Unit = {}\n\n    /**\n     * 出错，点击重试回调\n     */\n    var onReDownload: () -> Unit = {}\n\n    /**\n     * 通过浏览器下载APK包\n     */\n    fun downloadForWebView(url: String) {\n        val uri = Uri.parse(url)\n        val intent = Intent(Intent.ACTION_VIEW, uri)\n        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        context.startActivity(intent)\n    }\n\n    /**\n     * 出错后，点击重试\n     */\n    fun reDownload() {\n        onReDownload.invoke()\n        download()\n    }\n\n    /**\n     * App下载APK包，下载完成后安装\n     */\n    fun download() {\n\n        (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED).no {\n            log(\"没有SD卡\")\n            onError.invoke()\n            return\n        }\n\n        var filePath = \"\"\n        (updateInfo.config.apkSavePath.isNotEmpty()).yes {\n            filePath = updateInfo.config.apkSavePath\n        }.no {\n            // 适配Android10\n            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !Environment.isExternalStorageLegacy()){\n                filePath = (context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)?.absolutePath ?: \"\") + \"/apk\"\n            }else{\n                val packageName = context.packageName\n                filePath = Environment.getExternalStorageDirectory().absolutePath + \"/\" + packageName\n            }\n        }\n\n        // apk 保存名称\n        val apkName = if (updateInfo.config.apkSaveName.isNotEmpty()) {\n            updateInfo.config.apkSaveName\n        } else {\n            context.appName\n        }\n\n        val apkLocalPath = \"$filePath/$apkName.apk\"\n\n        downloadUpdateApkFilePath = apkLocalPath\n\n        SPUtil.putBase(KEY_OF_SP_APK_PATH, downloadUpdateApkFilePath)\n\n        FileDownloader.setup(context)\n\n        val downloadTask = FileDownloader.getImpl().create(updateInfo.apkUrl)\n            .setPath(apkLocalPath)\n\n        downloadTask\n            .addHeader(\"Accept-Encoding\",\"identity\")\n            .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\")\n            .setListener(object : FileDownloadLargeFileListener() {\n\n                override fun pending(task: BaseDownloadTask, soFarBytes: Long, totalBytes: Long) {\n                    log(\"----使用FileDownloader下载-------\")\n                    log(\"pending:soFarBytes($soFarBytes),totalBytes($totalBytes)\")\n                    downloadStart()\n                    if(totalBytes < 0){\n                        downloadTask.pause()\n                    }\n                }\n\n                override fun progress(task: BaseDownloadTask, soFarBytes: Long, totalBytes: Long) {\n                    downloading(soFarBytes, totalBytes)\n                    if(totalBytes < 0){\n                        downloadTask.pause()\n                    }\n                }\n\n                override fun paused(task: BaseDownloadTask, soFarBytes: Long, totalBytes: Long) {\n                    log(\"获取文件总长度失败出错，尝试HTTPURLConnection下载\")\n                    downloadUpdateApkFilePath.deleteFile()\n                    \"$downloadUpdateApkFilePath.temp\".deleteFile()\n                    downloadByHttpUrlConnection(filePath, apkName)\n                }\n\n                override fun completed(task: BaseDownloadTask) {\n                    downloadComplete()\n                }\n\n                override fun error(task: BaseDownloadTask, e: Throwable) {\n                    // FileDownloader 下载失败后，再调用 FileDownloadUtil 下载一次\n                    // FileDownloader 对码云或者阿里云上的apk文件会下载失败\n                    // downloadError(e)\n                    log(\"下载出错，尝试HTTPURLConnection下载\")\n                    downloadUpdateApkFilePath.deleteFile()\n                    \"$downloadUpdateApkFilePath.temp\".deleteFile()\n                    downloadByHttpUrlConnection(filePath, apkName)\n                }\n\n                override fun warn(task: BaseDownloadTask) {\n                }\n            }).start()\n    }\n\n    /**\n     * 使用 HttpUrlConnection 下载\n     */\n    private fun downloadByHttpUrlConnection(filePath: String, apkName: String?) {\n        FileDownloadUtil.download(\n            updateInfo.apkUrl,\n            filePath,\n            \"$apkName.apk\",\n            onStart = { downloadStart() },\n            onProgress = { current, total -> downloading(current, total) },\n            onComplete = { downloadComplete() },\n            onError = { downloadError(it) }\n        )\n    }\n\n    /**\n     * 开始下载逻辑\n     */\n    private fun downloadStart() {\n        isDownloading = true\n        UpdateAppUtils.downloadListener?.onStart()\n        UpdateAppReceiver.send(context, 0)\n    }\n\n    /**\n     * 下载中逻辑\n     */\n    private fun downloading(soFarBytes: Long, totalBytes: Long) {\n//        log(\"soFarBytes:$soFarBytes--totalBytes:$totalBytes\")\n        isDownloading = true\n        var progress = (soFarBytes * 100.0 / totalBytes).toInt()\n        if (progress < 0) progress = 0\n        log(\"progress:$progress\")\n        UpdateAppReceiver.send(context, progress)\n        this@DownloadAppUtils.onProgress.invoke(progress)\n        UpdateAppUtils.downloadListener?.onDownload(progress)\n    }\n\n    /**\n     * 下载完成处理逻辑\n     */\n    private fun downloadComplete() {\n        isDownloading = false\n        log(\"completed\")\n        this@DownloadAppUtils.onProgress.invoke(100)\n        UpdateAppUtils.downloadListener?.onFinish()\n        // 校验md5\n        (updateInfo.config.needCheckMd5).yes {\n            checkMd5(context)\n        }.no {\n            UpdateAppReceiver.send(context, 100)\n        }\n    }\n\n    /**\n     * 下载失败处理逻辑\n     */\n    private fun downloadError(e: Throwable) {\n        isDownloading = false\n        log(\"error:${e.message}\")\n        downloadUpdateApkFilePath.deleteFile()\n        this@DownloadAppUtils.onError.invoke()\n        UpdateAppUtils.downloadListener?.onError(e)\n        UpdateAppReceiver.send(context, -1000)\n    }\n\n    /**\n     * 校验Md5\n     *  先获取本应用的MD5值，获取未安装应用的MD5.进行对比\n     */\n    private fun checkMd5(context: Context) {\n        // 当前应用md5\n        val localMd5 = SignMd5Util.getAppSignatureMD5()\n\n        // 下载的apk 签名md5\n        val apkMd5 = SignMd5Util.getSignMD5FromApk(File(downloadUpdateApkFilePath))\n        log(\"当前应用签名md5：$localMd5\")\n        log(\"下载apk签名md5：$apkMd5\")\n\n        // 校验结果回调\n        UpdateAppUtils.md5CheckResultListener?.onResult(localMd5.equals(apkMd5, true))\n\n        (localMd5.equals(apkMd5, true)).yes {\n            log(\"md5校验成功\")\n            UpdateAppReceiver.send(context, 100)\n        }.no {\n            log(\"md5校验失败\")\n        }\n    }\n}"
  },
  {
    "path": "updateapputils/src/main/java/update/UpdateAppReceiver.kt",
    "content": "package update\n\nimport android.app.Notification\nimport android.app.NotificationChannel\nimport android.app.NotificationManager\nimport android.app.PendingIntent\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.graphics.BitmapFactory\nimport android.os.Build\nimport extension.installApk\nimport extension.no\nimport extension.yes\n\n/**\n * desc: UpdateAppReceiver\n * author: teprinciple on 2019/06/3.\n */\ninternal class UpdateAppReceiver : BroadcastReceiver() {\n\n    private val notificationChannel = \"1001\"\n\n    private val updateConfig by lazy { UpdateAppUtils.updateInfo.config }\n\n    private val uiConfig by lazy { UpdateAppUtils.updateInfo.uiConfig }\n\n    private var lastProgress = 0\n\n    override fun onReceive(context: Context, intent: Intent) {\n\n        when (intent.action) {\n\n            // 下载中\n            context.packageName + ACTION_UPDATE -> {\n                // 进度\n                val progress = intent.getIntExtra(KEY_OF_INTENT_PROGRESS, 0)\n\n                val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager\n\n                (progress != -1000).yes {\n                    lastProgress = progress\n                }\n\n                // 显示通知栏\n                val notifyId = 1\n                updateConfig.isShowNotification.yes {\n                    showNotification(context, notifyId, progress, notificationChannel, nm)\n                }\n\n                // 下载完成\n                if (progress == 100) {\n                    handleDownloadComplete(context, notifyId, nm)\n                }\n            }\n\n            // 重新下载\n            context.packageName + ACTION_RE_DOWNLOAD -> {\n                DownloadAppUtils.reDownload()\n            }\n        }\n    }\n\n    /**\n     * 下载完成后的逻辑\n     */\n    private fun handleDownloadComplete(context: Context, notifyId: Int, nm: NotificationManager?) {\n        // 关闭通知栏\n        nm?.let {\n            nm.cancel(notifyId)\n            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {\n                nm.deleteNotificationChannel(notificationChannel)\n            }\n        }\n\n        // 安装apk\n        context.installApk(DownloadAppUtils.downloadUpdateApkFilePath)\n    }\n\n    /**\n     * 通知栏显示\n     */\n    private fun showNotification(context: Context, notifyId: Int, progress: Int, notificationChannel: String, nm: NotificationManager) {\n\n        val notificationName = \"notification\"\n\n        // 适配 8.0\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            // 通知渠道\n            val channel = NotificationChannel(notificationChannel, notificationName, NotificationManager.IMPORTANCE_HIGH)\n            channel.enableLights(false)\n            // 是否在桌面icon右上角展示小红点\n            channel.setShowBadge(false)\n            // 是否在久按桌面图标时显示此渠道的通知\n            channel.enableVibration(false)\n            // 最后在notificationmanager中创建该通知渠道\n            nm.createNotificationChannel(channel)\n        }\n\n        val builder = Notification.Builder(context)\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n            builder.setChannelId(notificationChannel)\n        }\n\n\n        // 设置通知图标\n        (updateConfig.notifyImgRes > 0).yes {\n            builder.setSmallIcon(updateConfig.notifyImgRes)\n            builder.setLargeIcon(BitmapFactory.decodeResource(context.resources, updateConfig.notifyImgRes))\n        }.no {\n            builder.setSmallIcon(android.R.mipmap.sym_def_app_icon)\n        }\n\n        // 设置进度\n        builder.setProgress(100, lastProgress, false)\n\n        if (progress == -1000) {\n            val intent = Intent(context.packageName + ACTION_RE_DOWNLOAD)\n            intent.setPackage(context.packageName)\n            val pendingIntent = PendingIntent.getBroadcast(context, REQUEST_CODE, intent, PendingIntent.FLAG_CANCEL_CURRENT)\n            builder.setContentIntent(pendingIntent)\n            // 通知栏标题\n            builder.setContentTitle(uiConfig.downloadFailText)\n        } else {\n            // 通知栏标题\n            builder.setContentTitle(\"${uiConfig.downloadingBtnText}$progress%\")\n        }\n\n\n        // 设置只响一次\n        builder.setOnlyAlertOnce(true)\n        val notification = builder.build()\n        nm.notify(notifyId, notification)\n    }\n\n    companion object {\n        /**\n         * 进度key\n         */\n        private const val KEY_OF_INTENT_PROGRESS = \"KEY_OF_INTENT_PROGRESS\"\n\n        /**\n         * ACTION_UPDATE\n         */\n        const val ACTION_UPDATE = \"teprinciple.update\"\n\n        /**\n         * ACTION_RE_DOWNLOAD\n         */\n        const val ACTION_RE_DOWNLOAD = \"action_re_download\"\n\n\n        const val REQUEST_CODE = 1001\n\n\n        /**\n         * 发送进度通知\n         */\n        fun send(context: Context, progress: Int) {\n            val intent = Intent(context.packageName + ACTION_UPDATE)\n            intent.putExtra(KEY_OF_INTENT_PROGRESS, progress)\n            context.sendBroadcast(intent)\n        }\n    }\n}"
  },
  {
    "path": "updateapputils/src/main/java/update/UpdateAppService.kt",
    "content": "package update\n\nimport android.app.Service\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.os.IBinder\n\n/**\n * desc: UpdateAppService\n * author: teprinciple on 2018/11/3.\n */\ninternal class UpdateAppService : Service() {\n\n    private val updateAppReceiver = UpdateAppReceiver()\n\n    override fun onCreate() {\n        super.onCreate()\n        // 动态注册receiver 适配8.0 updateAppReceiver 静态注册没收不到广播\n        registerReceiver(updateAppReceiver, IntentFilter(packageName + UpdateAppReceiver.ACTION_UPDATE))\n        registerReceiver(updateAppReceiver, IntentFilter(packageName + UpdateAppReceiver.ACTION_RE_DOWNLOAD))\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        unregisterReceiver(updateAppReceiver) // 注销广播\n    }\n\n    override fun onBind(intent: Intent): IBinder? {\n        return null\n    }\n}\n"
  },
  {
    "path": "updateapputils/src/main/java/update/UpdateAppUtils.kt",
    "content": "package update\n\nimport android.content.Context\nimport extension.globalContext\nimport extension.log\nimport extension.no\nimport extension.yes\nimport listener.OnBtnClickListener\nimport listener.Md5CheckResultListener\nimport listener.OnInitUiListener\nimport listener.UpdateDownloadListener\nimport model.UiConfig\nimport model.UpdateConfig\nimport model.UpdateInfo\nimport ui.UpdateAppActivity\nimport util.GlobalContextProvider\nimport util.SPUtil\n\n\n/**\n * Created by Teprinciple on 2016/11/15.\n */\nobject UpdateAppUtils {\n\n    // 更新信息对象\n    internal val updateInfo by lazy { UpdateInfo() }\n\n    // 下载监听\n    internal var downloadListener: UpdateDownloadListener? = null\n\n    // md5校验结果回调\n    internal var md5CheckResultListener: Md5CheckResultListener? = null\n\n    // 初始化更新弹窗UI回调\n    internal var onInitUiListener: OnInitUiListener? = null\n\n    // \"暂不更新\"按钮点击事件\n    internal var onCancelBtnClickListener: OnBtnClickListener? = null\n\n    // \"立即更新\"按钮点击事件\n    internal var onUpdateBtnClickListener: OnBtnClickListener? = null\n\n    /**\n     * 设置apk下载地址\n     */\n    fun apkUrl(apkUrl: String): UpdateAppUtils {\n        updateInfo.apkUrl = apkUrl\n        return this\n    }\n\n    /**\n     * 设置更新标题\n     */\n    fun updateTitle(title: CharSequence): UpdateAppUtils {\n        updateInfo.updateTitle = title\n        return this\n    }\n\n    /**\n     * 设置更新内容\n     */\n    fun updateContent(content: CharSequence): UpdateAppUtils {\n        updateInfo.updateContent = content\n        return this\n    }\n\n    /**\n     * 设置更新配置\n     */\n    fun updateConfig(config: UpdateConfig): UpdateAppUtils {\n        updateInfo.config = config\n        return this\n    }\n\n    /**\n     * 设置UI配置\n     */\n    fun uiConfig(uiConfig: UiConfig): UpdateAppUtils {\n        updateInfo.uiConfig = uiConfig\n        return this\n    }\n\n    /**\n     * 设置下载监听\n     */\n    fun setUpdateDownloadListener(listener: UpdateDownloadListener?): UpdateAppUtils {\n        this.downloadListener = listener\n        return this\n    }\n\n    /**\n     * 设置md5校验结果监听\n     */\n    fun setMd5CheckResultListener(listener: Md5CheckResultListener?): UpdateAppUtils {\n        this.md5CheckResultListener = listener\n        return this\n    }\n\n    /**\n     * 设置初始化UI监听\n     */\n    fun setOnInitUiListener(listener: OnInitUiListener?): UpdateAppUtils {\n        this.onInitUiListener = listener\n        return this\n    }\n\n    /**\n     * 设置 “暂不更新” 按钮点击事件\n     */\n    fun setCancelBtnClickListener(listener: OnBtnClickListener?): UpdateAppUtils {\n        this.onCancelBtnClickListener = listener\n        return this\n    }\n\n    /**\n     * 设置 “立即更新” 按钮点击事件\n     */\n    fun setUpdateBtnClickListener(listener: OnBtnClickListener?): UpdateAppUtils {\n        this.onUpdateBtnClickListener = listener\n        return this\n    }\n\n    /**\n     * 检查更新\n     */\n    fun update() {\n\n        if(globalContext() == null){\n            log(\"请先调用初始化init\")\n            return\n        }\n\n        val keyName = (globalContext()?.packageName ?: \"\") + updateInfo.config.serverVersionName\n        // 设置每次显示，设置本次显示及强制更新 每次都显示弹窗\n        (updateInfo.config.alwaysShow || updateInfo.config.thisTimeShow || updateInfo.config.force).yes {\n            UpdateAppActivity.launch()\n        }.no {\n            val hasShow = SPUtil.getBoolean(keyName, false)\n            (hasShow).no { UpdateAppActivity.launch() }\n        }\n        SPUtil.putBase(keyName, true)\n    }\n\n    /* 未缓存apk\n    /**\n     * 删除已安装 apk\n     */\n    fun deleteInstalledApk() {\n        val apkPath = SPUtil.getString(DownloadAppUtils.KEY_OF_SP_APK_PATH, \"\")\n        val appVersionCode = Utils.getAPPVersionCode()\n        val apkVersionCode = Utils.getApkVersionCode(apkPath)\n        log(\"appVersionCode:$appVersionCode\")\n        log(\"apkVersionCode:$apkVersionCode\")\n        (apkPath.isNotEmpty() && appVersionCode == apkVersionCode && apkVersionCode > 0).yes {\n            Utils.deleteFile(apkPath)\n        }\n    }\n     */\n\n    /**\n     * 获取单例对象\n     */\n    @JvmStatic\n    fun getInstance() = this\n\n    /**\n     * 初始化，非必须。解决部分手机 通过UpdateFileProvider 获取不到context情况使用\n     * * @param context 提供全局context。\n     */\n    @JvmStatic\n    fun init(context: Context){\n        GlobalContextProvider.mContext = context.applicationContext\n        log(\"外部初始化context\")\n    }\n}"
  },
  {
    "path": "updateapputils/src/main/java/update/UpdateFileProvider.kt",
    "content": "package update\n\nimport android.support.v4.content.FileProvider\nimport extension.log\nimport extension.yes\nimport util.GlobalContextProvider\n\n/**\n * desc: UpdateFileProvider\n * time: 2019/7/10\n * @author Teprinciple\n */\nclass UpdateFileProvider : FileProvider() {\n    override fun onCreate(): Boolean {\n        val result = super.onCreate()\n        (GlobalContextProvider.mContext == null && context != null).yes {\n            GlobalContextProvider.mContext = context\n            log(\"内部Provider初始化context：\" + GlobalContextProvider.mContext)\n        }\n        return result\n    }\n}"
  },
  {
    "path": "updateapputils/src/main/java/util/AlertDialogUtil.kt",
    "content": "package util\n\nimport android.app.Activity\nimport android.app.AlertDialog\nimport com.teprinciple.updateapputils.R\nimport extension.string\n\n/**\n * desc: AlertDialogUtil\n * time: 2018/8/20\n * @author teprinciple\n */\ninternal object AlertDialogUtil {\n\n    fun show(\n        activity: Activity,\n        message: String,\n        onCancelClick: () -> Unit = {},\n        onSureClick: () -> Unit = {},\n        cancelable: Boolean = false,\n        title: String = string(R.string.notice),\n        cancelText: String = string(R.string.cancel),\n        sureText: String = string(R.string.sure)\n    ) {\n        AlertDialog.Builder(activity, R.style.AlertDialog)\n            .setTitle(title)\n            .setMessage(message)\n            .setPositiveButton(sureText) { _, _ ->\n                onSureClick.invoke()\n            }\n            .setNegativeButton(cancelText) { _, _ ->\n                onCancelClick.invoke()\n            }\n            .setCancelable(cancelable)\n            .create()\n            .show()\n    }\n}"
  },
  {
    "path": "updateapputils/src/main/java/util/FileDownloadUtil.kt",
    "content": "package util\n\nimport extension.log\nimport extension.no\nimport extension.yes\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.GlobalScope\nimport kotlinx.coroutines.launch\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.io.InputStream\nimport java.io.OutputStream\nimport java.net.HttpURLConnection\nimport java.net.HttpURLConnection.HTTP_OK\nimport java.net.URL\n\n/**\n * desc: 文件下载 当 FileDownloader 对某些apk下载失败时（比如：放在阿里云，码云上apk） 使用该工具类下载\n * time: 2019/8/28\n * @author teprinciple\n */\ninternal object FileDownloadUtil {\n\n    /**\n     * 下载文件\n     * @param url 文件地址\n     * @param fileSavePath 文件存储地址\n     * @param fileName 文件存储名称\n     * @param onStart 开始下载回调\n     * @param onProgress 下载中回调\n     * @param onComplete 下载完成回调\n     * @param onError 下载失败回调\n     */\n    fun download(\n        url: String,\n        fileSavePath: String,\n        fileName: String?,\n        onStart: () -> Unit = {},\n        onProgress: (current: Long, total: Long) -> Unit = { _, _ -> },\n        onComplete: () -> Unit = {},\n        onError: (Throwable) -> Unit = {}\n    ) {\n        GlobalScope.launch(Dispatchers.IO) {\n            log(\"----使用HttpURLConnection下载----\")\n            onStart.invoke()\n            var connection: HttpURLConnection? = null\n            var outputStream: FileOutputStream? = null\n\n            kotlin.runCatching {\n                connection = URL(url).openConnection() as HttpURLConnection\n                outputStream = FileOutputStream(File(fileSavePath, fileName))\n\n                connection?.apply {\n                    requestMethod = \"GET\"\n                    setRequestProperty(\"Charset\", \"utf-8\")\n                    setRequestProperty(\"Accept-Encoding\", \"identity\")\n                    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\")\n                    connect()\n                }\n\n                val responseCode = connection!!.responseCode\n                if (responseCode == HTTP_OK) {\n                    val total = connection!!.contentLength\n                    var progress = -1\n                    connection!!.inputStream.use { input ->\n                        outputStream.use { output ->\n                            input.copyToWithProgress(output!!) {\n                                val pro = (it * 100.0 / total).toInt()\n                                (progress != pro).yes {\n                                    GlobalScope.launch(Dispatchers.Main) {\n                                        onProgress(it, total.toLong())\n                                    }\n                                }\n                                progress = pro\n                            }\n                        }\n                    }\n                }else{\n                    throw Throwable(message = \"文件下载错误\")\n                }\n            }.onSuccess {\n                connection?.disconnect()\n                outputStream?.close()\n                log(\"HttpURLConnection下载完成\")\n                GlobalScope.launch(Dispatchers.Main) {\n                    (File(fileSavePath).length() > 0L).yes{\n                        onComplete.invoke()\n                    }.no {\n                        onError.invoke(Throwable(message = \"文件下载错误\"))\n                    }\n                }\n            }.onFailure {\n                connection?.disconnect()\n                outputStream?.close()\n                log(\"HttpURLConnection下载失败：${it.message}\")\n                GlobalScope.launch(Dispatchers.Main) {\n                    onError.invoke(it)\n                }\n            }\n        }\n    }\n}\n\nfun InputStream.copyToWithProgress(out: OutputStream, bufferSize: Int = DEFAULT_BUFFER_SIZE, currentByte: (Long) -> Unit = {}): Long {\n    var bytesCopied: Long = 0\n    val buffer = ByteArray(bufferSize)\n    var bytes = read(buffer)\n    while (bytes >= 0) {\n        out.write(buffer, 0, bytes)\n        bytesCopied += bytes\n        bytes = read(buffer)\n        currentByte.invoke(bytesCopied)\n    }\n    return bytesCopied\n}"
  },
  {
    "path": "updateapputils/src/main/java/util/GlobalContextProvider.kt",
    "content": "package util\n\nimport android.annotation.SuppressLint\nimport android.content.Context\n\n/**\n * desc: 提供context.\n */\n@SuppressLint(\"StaticFieldLeak\")\ninternal object GlobalContextProvider {\n\n    /** 全局context 提供扩展globalContext */\n    internal var mContext: Context? = null\n}"
  },
  {
    "path": "updateapputils/src/main/java/util/SPUtil.kt",
    "content": "package util\n\nimport android.app.Activity\nimport android.content.SharedPreferences\nimport extension.globalContext\n\n/**\n * SharedPreferences 数据保存\n */\ninternal object SPUtil {\n\n    fun putBase(keyName: String, value: Any): Boolean? {\n        val sharedPreferences = getSp()\n        val editor: SharedPreferences.Editor? = sharedPreferences?.edit()\n        when (value) {\n            is Int -> editor?.putInt(keyName, value)\n            is Boolean -> editor?.putBoolean(keyName, value)\n            is Float -> editor?.putFloat(keyName, value)\n            is String -> editor?.putString(keyName, value)\n            is Long -> editor?.putLong(keyName, value)\n            else -> throw IllegalArgumentException(\"SharedPreferences can,t be save this type\")\n        }\n        return editor?.commit()\n    }\n\n    fun getBoolean(keyName: String, defaultValue: Boolean = false): Boolean {\n        val sharedPreferences = getSp()\n        return sharedPreferences?.getBoolean(keyName, defaultValue) ?: false\n    }\n\n    fun getString(keyName: String, defaultValue: String? = null): String {\n        val sharedPreferences = getSp()\n        return sharedPreferences?.getString(keyName, defaultValue) ?: \"\"\n    }\n\n    private fun getSp(): SharedPreferences? {\n        if (globalContext() == null) return null\n        return globalContext()!!.getSharedPreferences(globalContext()!!.packageName, Activity.MODE_PRIVATE)\n    }\n}"
  },
  {
    "path": "updateapputils/src/main/java/util/SignMd5Util.kt",
    "content": "package util\n\nimport android.content.pm.PackageManager\nimport android.content.pm.Signature\nimport extension.globalContext\nimport java.io.File\nimport java.io.IOException\nimport java.security.MessageDigest\nimport java.security.NoSuchAlgorithmException\nimport java.security.cert.Certificate\nimport java.util.*\nimport java.util.jar.JarEntry\nimport java.util.jar.JarFile\nimport kotlin.experimental.and\n\n/**\n * desc: 获取签名 md5\n * time: 2019/6/21\n * @author teprinciple\n */\ninternal object SignMd5Util {\n\n    private val HEX_DIGITS = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F')\n\n    /**\n     * 获取当前应用签名文件md5\n     */\n    fun getAppSignatureMD5(): String {\n        val packageName = globalContext()?.packageName ?: \"\"\n        if (packageName.isEmpty()) return \"\"\n        val signature = getAppSignature(packageName)\n        return if (signature == null || signature.isEmpty()) {\n            \"\"\n        } else {\n            bytes2HexString(hashTemplate(signature[0].toByteArray(), \"MD5\"))\n                .replace(\"(?<=[0-9A-F]{2})[0-9A-F]{2}\".toRegex(), \":$0\")\n        }\n    }\n\n    /**\n     * 获取未安装apk 签名文件md5\n     */\n    fun getSignMD5FromApk(file: File): String {\n        val signatures = ArrayList<String>()\n        val jarFile = JarFile(file)\n        try {\n            val je = jarFile.getJarEntry(\"AndroidManifest.xml\")\n            val readBuffer = ByteArray(8192)\n            val certs = loadCertificates(jarFile, je, readBuffer)\n            if (certs != null) {\n                for (c in certs) {\n                    val sig = bytes2HexString(hashTemplate(c.encoded, \"MD5\"))\n                        .replace(\"(?<=[0-9A-F]{2})[0-9A-F]{2}\".toRegex(), \":$0\")\n                    signatures.add(sig)\n                }\n            }\n        } catch (ex: Exception) {\n        }\n        return signatures.getOrNull(0) ?: \"\"\n    }\n\n    private fun getAppSignature(packageName: String): Array<Signature>? {\n        if (packageName.isEmpty()) return null\n        return try {\n            val pm = globalContext()?.packageManager\n            val pi = pm?.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)\n            pi?.signatures\n        } catch (e: Exception) {\n            e.printStackTrace()\n            null\n        }\n    }\n\n    private fun hashTemplate(data: ByteArray?, algorithm: String): ByteArray? {\n        if (data == null || data.isEmpty()) return null\n        return try {\n            val md = MessageDigest.getInstance(algorithm)\n            md.update(data)\n            md.digest()\n        } catch (e: NoSuchAlgorithmException) {\n            e.printStackTrace()\n            null\n        }\n    }\n\n    private fun bytes2HexString(bytes: ByteArray?): String {\n        if (bytes == null) return \"\"\n        val len = bytes.size\n        if (len <= 0) return \"\"\n        val ret = CharArray(len shl 1)\n        var i = 0\n        var j = 0\n        while (i < len) {\n            ret[j++] = HEX_DIGITS[bytes[i].toInt().shr(4) and 0x0f]\n            ret[j++] = HEX_DIGITS[(bytes[i] and 0x0f).toInt()]\n            i++\n        }\n        return String(ret)\n    }\n\n    /**\n     * 加载签名\n     */\n    private fun loadCertificates(jarFile: JarFile, je: JarEntry?, readBuffer: ByteArray): Array<Certificate>? {\n        try {\n            val inputStream = jarFile.getInputStream(je)\n            while (inputStream.read(readBuffer, 0, readBuffer.size) != -1) {\n            }\n            inputStream.close()\n            return je?.certificates\n        } catch (e: IOException) {\n        }\n        return null\n    }\n}"
  },
  {
    "path": "updateapputils/src/main/res/anim/dialog_enter.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <alpha\n        android:duration=\"200\"\n        android:fromAlpha=\"0\"\n        android:toAlpha=\"1\"/>\n</set>"
  },
  {
    "path": "updateapputils/src/main/res/anim/dialog_out.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<set xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <alpha\n        android:duration=\"200\"\n        android:fromAlpha=\"1\"\n        android:toAlpha=\"0\"/>\n</set>"
  },
  {
    "path": "updateapputils/src/main/res/drawable/bg_update_btn.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <corners android:radius=\"6dp\"/>\n    <solid android:color=\"#1296db\"/>\n</shape>"
  },
  {
    "path": "updateapputils/src/main/res/drawable/bg_update_dialog.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:shape=\"rectangle\">\n    <solid android:color=\"@color/white\"/>\n    <corners android:radius=\"6dp\"/>\n</shape>"
  },
  {
    "path": "updateapputils/src/main/res/layout/view_update_dialog_plentiful.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<android.support.constraint.ConstraintLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"280dp\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"@drawable/bg_update_dialog\"\n    android:paddingBottom=\"10dp\">\n\n    <ImageView\n        android:id=\"@+id/iv_update_logo\"\n        android:layout_width=\"80dp\"\n        android:layout_height=\"80dp\"\n        android:layout_marginTop=\"15dp\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toTopOf=\"parent\"\n        app:srcCompat=\"@drawable/ic_update_logo\"/>\n\n    <TextView\n        android:id=\"@+id/tv_update_title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginTop=\"10dp\"\n        android:textColor=\"@color/text_title\"\n        android:textSize=\"16sp\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/iv_update_logo\"\n        tools:text=\"版本更新啦!\"/>\n\n    <ScrollView\n        android:id=\"@+id/scrollView2\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"90dp\"\n        android:layout_marginTop=\"10dp\"\n        android:overScrollMode=\"never\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/tv_update_title\">\n\n        <TextView\n            android:id=\"@+id/tv_update_content\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"top\"\n            android:gravity=\"start\"\n            android:lineSpacingExtra=\"5dp\"\n            android:paddingLeft=\"20dp\"\n            android:paddingRight=\"20dp\"\n            android:textColor=\"@color/text_content\"\n            android:textSize=\"14sp\"\n            tools:text=\"1、快来升级最新版本\\n2、这次更漂亮了\\n3、快点来吧\"/>\n    </ScrollView>\n\n    <TextView\n        android:id=\"@+id/btn_update_sure\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"35dp\"\n        android:layout_marginStart=\"20dp\"\n        android:layout_marginTop=\"10dp\"\n        android:layout_marginEnd=\"20dp\"\n        android:background=\"@drawable/bg_update_btn\"\n        android:gravity=\"center\"\n        android:text=\"@string/update_now\"\n        android:textColor=\"@color/white\"\n        android:textSize=\"14sp\"\n        app:layout_constraintBottom_toTopOf=\"@+id/btn_update_cancel\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/scrollView2\"\n        app:layout_goneMarginBottom=\"10dp\"/>\n\n    <TextView\n        android:id=\"@+id/btn_update_cancel\"\n        android:layout_width=\"0dp\"\n        android:layout_height=\"35dp\"\n        android:layout_marginStart=\"20dp\"\n        android:layout_marginTop=\"5dp\"\n        android:layout_marginEnd=\"20dp\"\n        android:gravity=\"center\"\n        android:text=\"@string/update_cancel\"\n        android:textColor=\"@color/text_content\"\n        android:textSize=\"14sp\"\n        app:layout_constraintBottom_toBottomOf=\"parent\"\n        app:layout_constraintEnd_toEndOf=\"parent\"\n        app:layout_constraintStart_toStartOf=\"parent\"\n        app:layout_constraintTop_toBottomOf=\"@+id/btn_update_sure\"\n        app:layout_goneMarginTop=\"10dp\"/>\n\n</android.support.constraint.ConstraintLayout>\n"
  },
  {
    "path": "updateapputils/src/main/res/layout/view_update_dialog_simple.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"280dp\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"@drawable/bg_update_dialog\"\n    android:orientation=\"vertical\">\n\n    <TextView\n        android:id=\"@+id/tv_update_title\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"wrap_content\"\n        android:layout_marginLeft=\"15dp\"\n        android:layout_marginTop=\"15dp\"\n        android:layout_marginRight=\"15dp\"\n        android:gravity=\"center_horizontal\"\n        android:textColor=\"@color/text_title\"\n        android:textSize=\"18sp\"\n        tools:text=\"版本更新啦!\"/>\n\n    <ScrollView\n        android:layout_marginTop=\"15dp\"\n        android:layout_marginBottom=\"10dp\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"90dp\"\n        android:overScrollMode=\"never\">\n\n        <TextView\n            android:id=\"@+id/tv_update_content\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_gravity=\"top\"\n            android:gravity=\"start\"\n            android:lineSpacingExtra=\"5dp\"\n            android:paddingLeft=\"12dp\"\n            android:paddingRight=\"12dp\"\n            android:textColor=\"@color/text_content\"\n            android:textSize=\"14sp\"/>\n    </ScrollView>\n\n    <View\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"1dp\"\n        android:background=\"#DFDFDF\"/>\n\n    <LinearLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"44dp\"\n        android:orientation=\"horizontal\">\n\n        <TextView\n            android:id=\"@+id/btn_update_cancel\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"1\"\n            android:gravity=\"center\"\n            android:text=\"@string/update_cancel\"\n            android:textColor=\"@color/text_content\"\n            android:textSize=\"16sp\"/>\n\n        <View\n            android:id=\"@+id/view_line\"\n            android:layout_width=\"1dp\"\n            android:layout_height=\"match_parent\"\n            android:background=\"#DFDFDF\"/>\n\n        <TextView\n            android:id=\"@+id/btn_update_sure\"\n            android:layout_width=\"0dp\"\n            android:layout_height=\"match_parent\"\n            android:layout_weight=\"1\"\n            android:gravity=\"center\"\n            android:text=\"@string/update_now\"\n            android:textColor=\"@color/text_blue\"\n            android:textSize=\"16sp\"/>\n    </LinearLayout>\n\n</LinearLayout>\n"
  },
  {
    "path": "updateapputils/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"white\">#ffffff</color>\n    <color name=\"text_blue\">#0076FF</color>\n    <color name=\"text_title\">#333333</color>\n    <color name=\"text_content\">#555555</color>\n</resources>\n"
  },
  {
    "path": "updateapputils/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"notice\">提示</string>\n    <string name=\"cancel\">取消</string>\n    <string name=\"sure\">确认</string>\n    <string name=\"update_now\">立即更新</string>\n    <string name=\"update_cancel\">暂不更新</string>\n    <string name=\"update_title\">版本更新啦！</string>\n    <string name=\"update_content\">发现新版本，立即更新</string>\n    <string name=\"download_fail\">下载出错，点击重试</string>\n    <string name=\"toast_download_apk\">更新下载中...</string>\n    <string name=\"downloading\">下载中</string>\n    <string name=\"no_storage_permission\">\"暂无储存权限，是否前往打开\"</string>\n    <string name=\"check_wifi_notice\">\"当前没有连接Wifi，是否继续下载\"</string>\n    <string name=\"install\">立即安装</string>\n</resources>\n"
  },
  {
    "path": "updateapputils/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <style name=\"CustomDialog\" parent=\"@android:style/Theme.Dialog\">\n        <item name=\"android:windowFrame\">@null</item>\n        <item name=\"android:windowIsFloating\">true</item>\n        <item name=\"android:windowIsTranslucent\">false</item>\n        <item name=\"android:windowNoTitle\">true</item>\n        <item name=\"android:windowBackground\">@drawable/bg_update_dialog</item>\n        <item name=\"android:backgroundDimEnabled\">true</item>\n    </style>\n\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.NoActionBar\"/>\n\n    <style name=\"DialogActivityTheme\" parent=\"Theme.AppCompat.Dialog\">\n        <item name=\"android:windowFrame\">@null</item>\n        <!-- 边框 -->\n        <item name=\"android:windowIsFloating\">true</item>\n        <!-- 是否浮现在activity之上 -->\n        <item name=\"android:windowIsTranslucent\">true</item>\n        <!-- 半透明 -->\n        <item name=\"android:windowNoTitle\">true</item>\n        <item name=\"android:windowBackground\">@android:color/transparent</item>\n        <item name=\"android:background\">@android:color/transparent</item>\n        <item name=\"android:backgroundDimEnabled\">true</item>\n        <!-- 模糊 -->\n        <item name=\"android:windowCloseOnTouchOutside\">false</item>\n        <!-- 重点，去掉标题 -->\n        <item name=\"windowNoTitle\">true</item>\n    </style>\n\n    <style name=\"AlertDialog\" parent=\"Base.Theme.AppCompat.Light.Dialog.Alert\">\n        <!--<item name=\"android:windowBackground\">@android:color/white</item>-->\n        <!--<item name=\"android:windowNoTitle\">true</item>-->\n    </style>\n</resources>"
  },
  {
    "path": "updateapputils/src/main/res/values-en/strings.xml",
    "content": "<resources>\n    <string name=\"notice\">Notice</string>\n    <string name=\"cancel\">Cancel</string>\n    <string name=\"sure\">OK</string>\n    <string name=\"update_now\">Update</string>\n    <string name=\"update_cancel\">Cancel</string>\n    <string name=\"update_title\">New version！</string>\n    <string name=\"update_content\">New version get ready,update now</string>\n    <string name=\"download_fail\">Download error, Click retry</string>\n    <string name=\"toast_download_apk\">Start downloading...</string>\n    <string name=\"downloading\">downloading</string>\n    <string name=\"no_storage_permission\">Please allow access to storage permissions</string>\n    <string name=\"check_wifi_notice\">Current net type is not Wifi, Whether to continue</string>\n    <string name=\"install\">Install</string>\n</resources>\n"
  },
  {
    "path": "updateapputils/src/main/res/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <base-config cleartextTrafficPermitted=\"true\" />\n</network-security-config>"
  },
  {
    "path": "updateapputils/src/main/res/xml/update_file_paths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths>\n    <external-path path=\"\" name=\"files_root\" />\n</paths>\n"
  }
]