瀏覽代碼

新增版本更新功能

yangyang 1 年之前
父節點
當前提交
098406ffe7
共有 71 個文件被更改,包括 10692 次插入0 次删除
  1. 1 0
      xupdate-lib/.gitignore
  2. 26 0
      xupdate-lib/build.gradle
  3. 21 0
      xupdate-lib/proguard-rules.pro
  4. 32 0
      xupdate-lib/src/main/AndroidManifest.xml
  5. 877 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/UpdateManager.java
  6. 414 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/XUpdate.java
  7. 336 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/_XUpdate.java
  8. 158 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/entity/DownloadEntity.java
  9. 189 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/entity/PromptEntity.java
  10. 320 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/entity/UpdateEntity.java
  11. 143 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/entity/UpdateError.java
  12. 20 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/listener/IUpdateParseCallback.java
  13. 47 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/listener/OnInstallListener.java
  14. 35 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/listener/OnUpdateFailureListener.java
  15. 83 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/listener/impl/DefaultInstallListener.java
  16. 35 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/listener/impl/DefaultUpdateFailureListener.java
  17. 37 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/logs/ILogger.java
  18. 134 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/logs/LogcatLogger.java
  19. 337 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/logs/UpdateLog.java
  20. 29 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IFileEncryptor.java
  21. 46 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IPrompterProxy.java
  22. 65 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IUpdateChecker.java
  23. 50 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IUpdateDownloader.java
  24. 120 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IUpdateHttpService.java
  25. 56 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IUpdateParser.java
  26. 40 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IUpdatePrompter.java
  27. 147 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IUpdateProxy.java
  28. 23 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/impl/AbstractUpdateParser.java
  29. 40 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/impl/DefaultFileEncryptor.java
  30. 61 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/impl/DefaultPrompterProxyImpl.java
  31. 155 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/impl/DefaultUpdateChecker.java
  32. 176 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/impl/DefaultUpdateDownloader.java
  33. 247 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/impl/DefaultUpdateParser.java
  34. 91 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/impl/DefaultUpdatePrompter.java
  35. 508 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/service/DownloadService.java
  36. 58 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/service/OnFileDownloadListener.java
  37. 347 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/ApkInstallUtils.java
  38. 170 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/ColorUtils.java
  39. 268 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/DialogUtils.java
  40. 299 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/DrawableUtils.java
  41. 428 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/FileUtils.java
  42. 79 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/Md5Utils.java
  43. 242 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/ShellUtils.java
  44. 23 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/UpdateFileProvider.java
  45. 470 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/UpdateUtils.java
  46. 185 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/widget/BaseDialog.java
  47. 56 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/widget/IDownloadEventHandler.java
  48. 515 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/widget/NumberProgressBar.java
  49. 445 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/widget/UpdateDialog.java
  50. 479 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/widget/UpdateDialogActivity.java
  51. 572 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/widget/UpdateDialogFragment.java
  52. 75 0
      xupdate-lib/src/main/java/com/xuexiang/xupdate/widget/WeakFileDownloadListener.java
  53. 8 0
      xupdate-lib/src/main/res/anim/xupdate_app_window_in.xml
  54. 9 0
      xupdate-lib/src/main/res/anim/xupdate_app_window_out.xml
  55. 二進制
      xupdate-lib/src/main/res/drawable-hdpi/xupdate_bg_app_top.png
  56. 二進制
      xupdate-lib/src/main/res/drawable-v17/xupdate_icon_app_update.png
  57. 26 0
      xupdate-lib/src/main/res/drawable-v21/xupdate_icon_app_update.xml
  58. 23 0
      xupdate-lib/src/main/res/drawable/xupdate_bg_app_info.xml
  59. 9 0
      xupdate-lib/src/main/res/drawable/xupdate_icon_app_close.xml
  60. 26 0
      xupdate-lib/src/main/res/drawable/xupdate_icon_app_update.xml
  61. 143 0
      xupdate-lib/src/main/res/layout-land/xupdate_layout_update_prompter.xml
  62. 144 0
      xupdate-lib/src/main/res/layout/xupdate_dialog_update.xml
  63. 144 0
      xupdate-lib/src/main/res/layout/xupdate_layout_update_prompter.xml
  64. 49 0
      xupdate-lib/src/main/res/values-en-rUS/xupdate_strings.xml
  65. 49 0
      xupdate-lib/src/main/res/values-zh-rCN/xupdate_strings.xml
  66. 40 0
      xupdate-lib/src/main/res/values/xupdate_attrs.xml
  67. 27 0
      xupdate-lib/src/main/res/values/xupdate_colors.xml
  68. 19 0
      xupdate-lib/src/main/res/values/xupdate_dimens.xml
  69. 48 0
      xupdate-lib/src/main/res/values/xupdate_strings.xml
  70. 80 0
      xupdate-lib/src/main/res/values/xupdate_style_widget.xml
  71. 38 0
      xupdate-lib/src/main/res/xml/update_file_paths.xml

+ 1 - 0
xupdate-lib/.gitignore

@@ -0,0 +1 @@
+/build

+ 26 - 0
xupdate-lib/build.gradle

@@ -0,0 +1,26 @@
+apply plugin: 'com.android.library'
+//apply plugin: 'img-optimizer'
+
+android {
+    compileSdkVersion 30
+    buildToolsVersion "30.0.1"
+
+    defaultConfig {
+        minSdkVersion 21
+        targetSdkVersion 30
+
+        vectorDrawables.useSupportLibrary = true
+    }
+
+    lintOptions {
+        checkReleaseBuilds false
+        abortOnError false
+    }
+
+}
+
+dependencies {
+    implementation fileTree(dir: 'libs', include: ['*.jar'])
+    implementation 'androidx.appcompat:appcompat:1.2.0'
+}
+

+ 21 - 0
xupdate-lib/proguard-rules.pro

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

+ 32 - 0
xupdate-lib/src/main/AndroidManifest.xml

@@ -0,0 +1,32 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.xuexiang.xupdate">
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission
+        android:name="android.permission.INSTALL_PACKAGES"
+        tools:ignore="ProtectedPermissions" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+
+    <application>
+        <service android:name="com.xuexiang.xupdate.service.DownloadService" />
+
+        <provider
+            android:name=".utils.UpdateFileProvider"
+            android:authorities="${applicationId}.updateFileProvider"
+            android:exported="false"
+            android:grantUriPermissions="true">
+            <meta-data
+                android:name="android.support.FILE_PROVIDER_PATHS"
+                android:resource="@xml/update_file_paths" />
+        </provider>
+
+        <activity
+            android:name=".widget.UpdateDialogActivity"
+            android:theme="@style/XUpdate_DialogTheme" />
+
+    </application>
+</manifest>

+ 877 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/UpdateManager.java

@@ -0,0 +1,877 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+
+import com.xuexiang.xupdate.entity.PromptEntity;
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.listener.IUpdateParseCallback;
+import com.xuexiang.xupdate.logs.UpdateLog;
+import com.xuexiang.xupdate.proxy.IUpdateChecker;
+import com.xuexiang.xupdate.proxy.IUpdateDownloader;
+import com.xuexiang.xupdate.proxy.IUpdateHttpService;
+import com.xuexiang.xupdate.proxy.IUpdateParser;
+import com.xuexiang.xupdate.proxy.IUpdatePrompter;
+import com.xuexiang.xupdate.proxy.IUpdateProxy;
+import com.xuexiang.xupdate.proxy.impl.DefaultUpdatePrompter;
+import com.xuexiang.xupdate.service.OnFileDownloadListener;
+import com.xuexiang.xupdate.utils.UpdateUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.Map;
+import java.util.TreeMap;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.CHECK_NO_NETWORK;
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.CHECK_NO_WIFI;
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.PROMPT_ACTIVITY_DESTROY;
+
+/**
+ * 版本更新管理者
+ *
+ * @author xuexiang
+ * @since 2018/7/1 下午9:49
+ */
+public class UpdateManager implements IUpdateProxy {
+    /**
+     * 版本更新代理
+     */
+    private IUpdateProxy mUpdateProxy;
+    /**
+     * 更新信息
+     */
+    private UpdateEntity mUpdateEntity;
+
+    /**
+     * 上下文
+     */
+    private final WeakReference<Context> mContext;
+    //============请求参数==============//
+    /**
+     * 版本更新的url地址
+     */
+    private final String mUpdateUrl;
+    /**
+     * 请求参数
+     */
+    private final Map<String, Object> mParams;
+
+    /**
+     * apk缓存的目录
+     */
+    private final String mApkCacheDir;
+
+    //===========更新模式================//
+    /**
+     * 是否只在wifi下进行版本更新检查
+     */
+    private final boolean mIsWifiOnly;
+    /**
+     * 是否是Get请求
+     */
+    private final boolean mIsGet;
+    /**
+     * 是否是自动版本更新模式【无人干预,自动下载,自动更新】
+     */
+    private final boolean mIsAutoMode;
+    //===========更新组件===============//
+    /**
+     * 版本更新网络请求服务API
+     */
+    private IUpdateHttpService mIUpdateHttpService;
+    /**
+     * 版本更新检查器
+     */
+    private final IUpdateChecker mIUpdateChecker;
+    /**
+     * 版本更新解析器
+     */
+    private final IUpdateParser mIUpdateParser;
+    /**
+     * 版本更新下载器
+     */
+    private IUpdateDownloader mIUpdateDownloader;
+    /**
+     * 文件下载监听
+     */
+    private OnFileDownloadListener mOnFileDownloadListener;
+    /**
+     * 版本更新提示器
+     */
+    private final IUpdatePrompter mIUpdatePrompter;
+    /**
+     * 版本更新提示器参数信息
+     */
+    private final PromptEntity mPromptEntity;
+
+
+    /**
+     * 构造函数
+     *
+     * @param builder 版本更新管理构建者
+     */
+    private UpdateManager(Builder builder) {
+        mContext = new WeakReference<>(builder.context);
+        mUpdateUrl = builder.updateUrl;
+        mParams = builder.params;
+        mApkCacheDir = builder.apkCacheDir;
+
+        mIsWifiOnly = builder.isWifiOnly;
+        mIsGet = builder.isGet;
+        mIsAutoMode = builder.isAutoMode;
+
+        mIUpdateHttpService = builder.updateHttpService;
+
+        mIUpdateChecker = builder.updateChecker;
+        mIUpdateParser = builder.updateParser;
+        mIUpdateDownloader = builder.updateDownLoader;
+        mOnFileDownloadListener = builder.onFileDownloadListener;
+
+        mIUpdatePrompter = builder.updatePrompter;
+        mPromptEntity = builder.promptEntity;
+    }
+
+    /**
+     * 设置版本更新的代理,可自定义版本更新
+     *
+     * @param updateProxy 版本更新的代理
+     * @return 版本更新管理者
+     */
+    public UpdateManager setIUpdateProxy(IUpdateProxy updateProxy) {
+        mUpdateProxy = updateProxy;
+        return this;
+    }
+
+    @Nullable
+    @Override
+    public Context getContext() {
+        return mContext.get();
+    }
+
+    @Override
+    public String getUrl() {
+        return mUpdateUrl;
+    }
+
+    @Override
+    public IUpdateHttpService getIUpdateHttpService() {
+        return mIUpdateHttpService;
+    }
+
+    /**
+     * 开始版本更新
+     */
+    @Override
+    public void update() {
+        UpdateLog.d("XUpdate.update()启动:" + this);
+        if (mUpdateProxy != null) {
+            mUpdateProxy.update();
+        } else {
+            doUpdate();
+        }
+    }
+
+    /**
+     * 执行版本更新操作
+     */
+    private void doUpdate() {
+        onBeforeCheck();
+
+        doCheck();
+    }
+
+    private void doCheck() {
+        if (mIsWifiOnly) {
+            if (UpdateUtils.checkWifi()) {
+                checkVersion();
+            } else {
+                onAfterCheck();
+                _XUpdate.onUpdateError(CHECK_NO_WIFI);
+            }
+        } else {
+            if (UpdateUtils.checkNetwork()) {
+                checkVersion();
+            } else {
+                onAfterCheck();
+                _XUpdate.onUpdateError(CHECK_NO_NETWORK);
+            }
+        }
+    }
+
+    /**
+     * 版本检查之前
+     */
+    @Override
+    public void onBeforeCheck() {
+        if (mUpdateProxy != null) {
+            mUpdateProxy.onBeforeCheck();
+        } else {
+            mIUpdateChecker.onBeforeCheck();
+        }
+    }
+
+    /**
+     * 执行网络请求,检查应用的版本信息
+     */
+    @Override
+    public void checkVersion() {
+        UpdateLog.d("开始检查版本信息...");
+        if (mUpdateProxy != null) {
+            mUpdateProxy.checkVersion();
+        } else {
+            if (TextUtils.isEmpty(mUpdateUrl)) {
+                throw new NullPointerException("[UpdateManager] : mUpdateUrl 不能为空");
+            }
+            mIUpdateChecker.checkVersion(mIsGet, mUpdateUrl, mParams, this);
+        }
+    }
+
+    /**
+     * 版本检查之后
+     */
+    @Override
+    public void onAfterCheck() {
+        if (mUpdateProxy != null) {
+            mUpdateProxy.onAfterCheck();
+        } else {
+            mIUpdateChecker.onAfterCheck();
+        }
+    }
+
+    @Override
+    public boolean isAsyncParser() {
+        if (mUpdateProxy != null) {
+            return mUpdateProxy.isAsyncParser();
+        } else {
+            return mIUpdateParser.isAsyncParser();
+        }
+    }
+
+    @Override
+    public UpdateEntity parseJson(@NonNull String json) throws Exception {
+        UpdateLog.i("服务端返回的最新版本信息:" + json);
+        if (mUpdateProxy != null) {
+            mUpdateEntity = mUpdateProxy.parseJson(json);
+        } else {
+            mUpdateEntity = mIUpdateParser.parseJson(json);
+        }
+        mUpdateEntity = refreshParams(mUpdateEntity);
+        return mUpdateEntity;
+    }
+
+    @Override
+    public void parseJson(@NonNull String json, final IUpdateParseCallback callback) throws Exception {
+        UpdateLog.i("服务端返回的最新版本信息:" + json);
+        if (mUpdateProxy != null) {
+            mUpdateProxy.parseJson(json, new IUpdateParseCallback() {
+                @Override
+                public void onParseResult(UpdateEntity updateEntity) {
+                    mUpdateEntity = refreshParams(updateEntity);
+                    callback.onParseResult(updateEntity);
+                }
+            });
+        } else {
+            mIUpdateParser.parseJson(json, new IUpdateParseCallback() {
+                @Override
+                public void onParseResult(UpdateEntity updateEntity) {
+                    mUpdateEntity = refreshParams(updateEntity);
+                    callback.onParseResult(updateEntity);
+                }
+            });
+        }
+    }
+
+    /**
+     * 刷新本地参数
+     *
+     * @param updateEntity 版本更新信息
+     */
+    private UpdateEntity refreshParams(UpdateEntity updateEntity) {
+        //更新信息(本地信息)
+        if (updateEntity != null) {
+            updateEntity.setApkCacheDir(mApkCacheDir);
+            updateEntity.setIsAutoMode(mIsAutoMode);
+            updateEntity.setIUpdateHttpService(mIUpdateHttpService);
+        }
+        return updateEntity;
+    }
+
+    /**
+     * 发现新版本
+     *
+     * @param updateEntity 版本更新信息
+     * @param updateProxy  版本更新代理
+     */
+    @Override
+    public void findNewVersion(@NonNull UpdateEntity updateEntity, @NonNull IUpdateProxy updateProxy) {
+        UpdateLog.i("发现新版本:" + updateEntity);
+        if (updateEntity.isSilent()) {
+            //静默下载,发现新版本后,直接下载更新
+            if (!UpdateUtils.isApkDownloaded(updateEntity)) {
+                startDownload(updateEntity, mOnFileDownloadListener);
+            } else {
+                //已经下载好的直接安装
+                _XUpdate.startInstallApk(getContext(), UpdateUtils.getApkFileByUpdateEntity(mUpdateEntity), mUpdateEntity.getDownLoadEntity());
+            }
+        } else {
+            if (mUpdateProxy != null) {
+                //否则显示版本更新提示
+                mUpdateProxy.findNewVersion(updateEntity, updateProxy);
+            } else {
+                if (mIUpdatePrompter instanceof DefaultUpdatePrompter) {
+                    Context context = getContext();
+                    if (context instanceof FragmentActivity && ((FragmentActivity) context).isFinishing()) {
+                        _XUpdate.onUpdateError(PROMPT_ACTIVITY_DESTROY);
+                    } else {
+                        mIUpdatePrompter.showPrompt(updateEntity, updateProxy, mPromptEntity);
+                    }
+                } else {
+                    mIUpdatePrompter.showPrompt(updateEntity, updateProxy, mPromptEntity);
+                }
+            }
+        }
+    }
+
+    /**
+     * 未发现新版本
+     *
+     * @param throwable 未发现的原因
+     */
+    @Override
+    public void noNewVersion(Throwable throwable) {
+        UpdateLog.i(throwable != null ? "未发现新版本:" + throwable.getMessage() : "未发现新版本!");
+        if (mUpdateProxy != null) {
+            mUpdateProxy.noNewVersion(throwable);
+        } else {
+            mIUpdateChecker.noNewVersion(throwable);
+        }
+    }
+
+    @Override
+    public void startDownload(@NonNull UpdateEntity updateEntity, @Nullable OnFileDownloadListener downloadListener) {
+        UpdateLog.i("开始下载更新文件:" + updateEntity);
+        updateEntity.setIUpdateHttpService(mIUpdateHttpService);
+        if (mUpdateProxy != null) {
+            mUpdateProxy.startDownload(updateEntity, downloadListener);
+        } else {
+            if (mIUpdateDownloader != null) {
+                mIUpdateDownloader.startDownload(updateEntity, downloadListener);
+            }
+        }
+    }
+
+    /**
+     * 后台下载
+     */
+    @Override
+    public void backgroundDownload() {
+        UpdateLog.i("点击了后台更新按钮, 在通知栏中显示下载进度...");
+        if (mUpdateProxy != null) {
+            mUpdateProxy.backgroundDownload();
+        } else {
+            if (mIUpdateDownloader != null) {
+                mIUpdateDownloader.backgroundDownload();
+            }
+        }
+    }
+
+    @Override
+    public void cancelDownload() {
+        UpdateLog.d("正在取消更新文件的下载...");
+        if (mUpdateProxy != null) {
+            mUpdateProxy.cancelDownload();
+        } else {
+            if (mIUpdateDownloader != null) {
+                mIUpdateDownloader.cancelDownload();
+            }
+        }
+    }
+
+    @Override
+    public void recycle() {
+        UpdateLog.d("正在回收资源...");
+        if (mUpdateProxy != null) {
+            mUpdateProxy.recycle();
+            mUpdateProxy = null;
+        }
+        if (mParams != null) {
+            mParams.clear();
+        }
+        mIUpdateHttpService = null;
+        mIUpdateDownloader = null;
+        mOnFileDownloadListener = null;
+    }
+
+    //============================对外提供的自定义使用api===============================//
+
+    /**
+     * 为外部提供简单的下载功能
+     *
+     * @param downloadUrl      下载地址
+     * @param downloadListener 下载监听
+     */
+    public void download(String downloadUrl, @Nullable OnFileDownloadListener downloadListener) {
+        startDownload(refreshParams(new UpdateEntity().setDownloadUrl(downloadUrl)), downloadListener);
+    }
+
+    /**
+     * 直接更新,不使用版本更新检查器
+     *
+     * @param updateEntity 版本更新信息
+     */
+    public void update(UpdateEntity updateEntity) {
+        mUpdateEntity = refreshParams(updateEntity);
+        try {
+            UpdateUtils.processUpdateEntity(mUpdateEntity, "这里调用的是直接更新方法,因此没有json!", this);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+
+    //============================构建者===============================//
+
+    /**
+     * 版本更新管理构建者
+     */
+    public static class Builder {
+        //=======必填项========//
+        Context context;
+        /**
+         * 版本更新的url地址
+         */
+        String updateUrl;
+        /**
+         * 请求参数
+         */
+        Map<String, Object> params;
+        /**
+         * 版本更新网络请求服务API
+         */
+        IUpdateHttpService updateHttpService;
+        /**
+         * 版本更新解析器
+         */
+        IUpdateParser updateParser;
+        //===========更新模式================//
+        /**
+         * 是否使用的是Get请求
+         */
+        boolean isGet;
+        /**
+         * 是否只在wifi下进行版本更新检查
+         */
+        boolean isWifiOnly;
+        /**
+         * 是否是自动版本更新模式【无人干预,有版本更新直接下载、安装】
+         */
+        boolean isAutoMode;
+
+        //===========更新行为================//
+        /**
+         * 版本更新检查器
+         */
+        IUpdateChecker updateChecker;
+        /**
+         * 版本更新提示器参数信息
+         */
+        PromptEntity promptEntity;
+        /**
+         * 版本更新提示器
+         */
+        IUpdatePrompter updatePrompter;
+        /**
+         * 下载器
+         */
+        IUpdateDownloader updateDownLoader;
+        /**
+         * 下载监听
+         */
+        OnFileDownloadListener onFileDownloadListener;
+        /**
+         * apk缓存的目录
+         */
+        String apkCacheDir;
+
+        /**
+         * 构建者
+         *
+         * @param context 上下文
+         */
+        Builder(@NonNull Context context) {
+            this.context = context;
+
+            params = new TreeMap<>();
+            if (_XUpdate.getParams() != null) {
+                params.putAll(_XUpdate.getParams());
+            }
+
+            promptEntity = new PromptEntity();
+
+            updateHttpService = _XUpdate.getIUpdateHttpService();
+
+            updateChecker = _XUpdate.getIUpdateChecker();
+            updateParser = _XUpdate.getIUpdateParser();
+            updatePrompter = _XUpdate.getIUpdatePrompter();
+            updateDownLoader = _XUpdate.getIUpdateDownLoader();
+
+            isGet = _XUpdate.isGet();
+            isWifiOnly = _XUpdate.isWifiOnly();
+            isAutoMode = _XUpdate.isAutoMode();
+            apkCacheDir = _XUpdate.getApkCacheDir();
+        }
+
+        /**
+         * 设置版本更新检查的url
+         *
+         * @param updateUrl 版本更新检查的url
+         * @return this
+         */
+        public Builder updateUrl(@NonNull String updateUrl) {
+            this.updateUrl = updateUrl;
+            return this;
+        }
+
+        /**
+         * 设置请求参数
+         *
+         * @param params 请求参数
+         * @return this
+         */
+        public Builder params(@NonNull Map<String, Object> params) {
+            this.params.putAll(params);
+            return this;
+        }
+
+        /**
+         * 设置请求参数
+         *
+         * @param key   键
+         * @param value 值
+         * @return this
+         */
+        public Builder param(@NonNull String key, @NonNull Object value) {
+            this.params.put(key, value);
+            return this;
+        }
+
+        /**
+         * 设置网络请求的请求服务API
+         *
+         * @param updateHttpService 网络请求的请求服务API
+         * @return this
+         */
+        public Builder updateHttpService(@NonNull IUpdateHttpService updateHttpService) {
+            this.updateHttpService = updateHttpService;
+            return this;
+        }
+
+        /**
+         * 设置apk下载的缓存目录
+         *
+         * @param apkCacheDir apk下载的缓存目录
+         * @return this
+         */
+        public Builder apkCacheDir(@NonNull String apkCacheDir) {
+            this.apkCacheDir = apkCacheDir;
+            return this;
+        }
+
+        /**
+         * 是否使用Get请求
+         *
+         * @param isGet 是否使用Get请求
+         * @return this
+         */
+        public Builder isGet(boolean isGet) {
+            this.isGet = isGet;
+            return this;
+        }
+
+        /**
+         * 设置是否是自动版本更新模式【无人干预,有版本更新直接下载、安装,需要root权限】
+         *
+         * @param isAutoMode 是否是自动版本更新模式
+         * @return this
+         */
+        public Builder isAutoMode(boolean isAutoMode) {
+            this.isAutoMode = isAutoMode;
+            return this;
+        }
+
+        /**
+         * 设置是否只在wifi下进行版本更新检查
+         *
+         * @param isWifiOnly 是否只在wifi下进行版本更新检查
+         * @return this
+         */
+        public Builder isWifiOnly(boolean isWifiOnly) {
+            this.isWifiOnly = isWifiOnly;
+            return this;
+        }
+
+        /**
+         * 设置版本更新检查器
+         *
+         * @param updateChecker 版本更新检查器
+         * @return this
+         */
+        public Builder updateChecker(@NonNull IUpdateChecker updateChecker) {
+            this.updateChecker = updateChecker;
+            return this;
+        }
+
+        /**
+         * 设置版本更新的解析器
+         *
+         * @param updateParser 版本更新的解析器
+         * @return this
+         */
+        public Builder updateParser(@NonNull IUpdateParser updateParser) {
+            this.updateParser = updateParser;
+            return this;
+        }
+
+        /**
+         * 设置版本更新提示器
+         *
+         * @param updatePrompter 版本更新提示器
+         * @return this
+         */
+        public Builder updatePrompter(@NonNull IUpdatePrompter updatePrompter) {
+            this.updatePrompter = updatePrompter;
+            return this;
+        }
+
+        /**
+         * 设置文件的下载监听
+         *
+         * @param onFileDownloadListener 文件下载监听
+         * @return this
+         */
+        public Builder setOnFileDownloadListener(OnFileDownloadListener onFileDownloadListener) {
+            this.onFileDownloadListener = onFileDownloadListener;
+            return this;
+        }
+
+        /**
+         * 设置主题颜色
+         *
+         * @param themeColor 主题颜色资源
+         * @return this
+         */
+        @Deprecated
+        public Builder themeColor(@ColorInt int themeColor) {
+            promptEntity.setThemeColor(themeColor);
+            return this;
+        }
+
+        /**
+         * 设置主题颜色
+         *
+         * @param themeColor 主题颜色资源
+         * @return this
+         */
+        public Builder promptThemeColor(@ColorInt int themeColor) {
+            promptEntity.setThemeColor(themeColor);
+            return this;
+        }
+
+        /**
+         * 设置顶部背景图片
+         *
+         * @param topResId 顶部背景图片资源
+         * @return this
+         */
+        @Deprecated
+        public Builder topResId(@DrawableRes int topResId) {
+            promptEntity.setTopResId(topResId);
+            return this;
+        }
+
+        /**
+         * 设置顶部背景图片
+         *
+         * @param topResId 顶部背景图片资源
+         * @return this
+         */
+        public Builder promptTopResId(@DrawableRes int topResId) {
+            promptEntity.setTopResId(topResId);
+            return this;
+        }
+
+        /**
+         * 设置顶部背景图片
+         *
+         * @param topDrawable 顶部背景图片
+         * @return this
+         */
+        public Builder promptTopDrawable(Drawable topDrawable) {
+            if (topDrawable != null) {
+                String tag = _XUpdate.saveTopDrawable(topDrawable);
+                promptEntity.setTopDrawableTag(tag);
+            }
+            return this;
+        }
+
+        /**
+         * 设置顶部背景图片
+         *
+         * @param topBitmap 顶部背景图片
+         * @return this
+         */
+        public Builder promptTopBitmap(Bitmap topBitmap) {
+            if (topBitmap != null) {
+                String tag = _XUpdate.saveTopDrawable(new BitmapDrawable(context.getResources(), topBitmap));
+                promptEntity.setTopDrawableTag(tag);
+            }
+            return this;
+        }
+
+        /**
+         * 设置按钮的文字颜色
+         *
+         * @param buttonTextColor 按钮的文字颜色
+         * @return this
+         */
+        public Builder promptButtonTextColor(@ColorInt int buttonTextColor) {
+            promptEntity.setButtonTextColor(buttonTextColor);
+            return this;
+        }
+
+        /**
+         * 设置是否支持后台更新
+         *
+         * @param supportBackgroundUpdate 是否支持后台更新
+         * @return this
+         */
+        public Builder supportBackgroundUpdate(boolean supportBackgroundUpdate) {
+            promptEntity.setSupportBackgroundUpdate(supportBackgroundUpdate);
+            return this;
+        }
+
+        /**
+         * 设置版本更新提示器宽度占屏幕的比例,默认是-1,不做约束
+         *
+         * @param widthRatio 提示器宽度占屏幕的比例
+         * @return this
+         */
+        public Builder promptWidthRatio(float widthRatio) {
+            promptEntity.setWidthRatio(widthRatio);
+            return this;
+        }
+
+        /**
+         * 设置版本更新提示器高度占屏幕的比例,默认是-1,不做约束
+         *
+         * @param heightRatio 提示器高度占屏幕的比例
+         * @return this
+         */
+        public Builder promptHeightRatio(float heightRatio) {
+            promptEntity.setHeightRatio(heightRatio);
+            return this;
+        }
+
+        /**
+         * 设置是否忽略下载异常【【为true时,下载失败更新提示框不消失,默认是false】】
+         *
+         * @param ignoreDownloadError 提器高度占屏幕的比例
+         * @return this
+         */
+        public Builder promptIgnoreDownloadError(boolean ignoreDownloadError) {
+            promptEntity.setIgnoreDownloadError(ignoreDownloadError);
+            return this;
+        }
+
+        /**
+         * 设置版本更新提示器的样式
+         *
+         * @param promptEntity 版本更新提示器参数信息
+         * @return this
+         */
+        public Builder promptStyle(@NonNull PromptEntity promptEntity) {
+            this.promptEntity = promptEntity;
+            return this;
+        }
+
+        /**
+         * 设备版本更新下载器
+         *
+         * @param updateDownLoader 版本更新下载器
+         * @return this
+         */
+        public Builder updateDownLoader(@NonNull IUpdateDownloader updateDownLoader) {
+            this.updateDownLoader = updateDownLoader;
+            return this;
+        }
+
+        /**
+         * 构建版本更新管理者
+         *
+         * @return 版本更新管理者
+         */
+        public UpdateManager build() {
+            UpdateUtils.requireNonNull(this.context, "[UpdateManager.Builder] : context == null");
+            UpdateUtils.requireNonNull(this.updateHttpService, "[UpdateManager.Builder] : updateHttpService == null");
+
+            if (TextUtils.isEmpty(apkCacheDir)) {
+                apkCacheDir = UpdateUtils.getDefaultDiskCacheDirPath();
+            }
+            return new UpdateManager(this);
+        }
+
+        /**
+         * 进行版本更新
+         */
+        public void update() {
+            build().update();
+        }
+
+        /**
+         * 进行版本更新
+         *
+         * @param updateProxy 版本更新代理
+         */
+        public void update(IUpdateProxy updateProxy) {
+            build().setIUpdateProxy(updateProxy)
+                    .update();
+        }
+    }
+
+    @NonNull
+    @Override
+    public String toString() {
+        return "XUpdate{" +
+                "mUpdateUrl='" + mUpdateUrl + '\'' +
+                ", mParams=" + mParams +
+                ", mApkCacheDir='" + mApkCacheDir + '\'' +
+                ", mIsWifiOnly=" + mIsWifiOnly +
+                ", mIsGet=" + mIsGet +
+                ", mIsAutoMode=" + mIsAutoMode +
+                '}';
+    }
+}

+ 414 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/XUpdate.java

@@ -0,0 +1,414 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate;
+
+import android.app.Application;
+import android.content.Context;
+
+import com.xuexiang.xupdate.entity.UpdateError;
+import com.xuexiang.xupdate.listener.OnInstallListener;
+import com.xuexiang.xupdate.listener.OnUpdateFailureListener;
+import com.xuexiang.xupdate.listener.impl.DefaultInstallListener;
+import com.xuexiang.xupdate.listener.impl.DefaultUpdateFailureListener;
+import com.xuexiang.xupdate.logs.ILogger;
+import com.xuexiang.xupdate.logs.UpdateLog;
+import com.xuexiang.xupdate.proxy.IFileEncryptor;
+import com.xuexiang.xupdate.proxy.IUpdateChecker;
+import com.xuexiang.xupdate.proxy.IUpdateDownloader;
+import com.xuexiang.xupdate.proxy.IUpdateHttpService;
+import com.xuexiang.xupdate.proxy.IUpdateParser;
+import com.xuexiang.xupdate.proxy.IUpdatePrompter;
+import com.xuexiang.xupdate.proxy.impl.DefaultFileEncryptor;
+import com.xuexiang.xupdate.proxy.impl.DefaultUpdateChecker;
+import com.xuexiang.xupdate.proxy.impl.DefaultUpdateDownloader;
+import com.xuexiang.xupdate.proxy.impl.DefaultUpdateParser;
+import com.xuexiang.xupdate.proxy.impl.DefaultUpdatePrompter;
+import com.xuexiang.xupdate.utils.ApkInstallUtils;
+
+import java.util.Map;
+import java.util.TreeMap;
+
+import androidx.annotation.NonNull;
+
+/**
+ * 版本更新的入口
+ *
+ * @author xuexiang
+ * @since 2018/6/29 下午7:47
+ */
+public class XUpdate {
+
+    private Application mContext;
+    private static XUpdate sInstance;
+
+    //========全局属性==========//
+    /**
+     * 请求参数【比如apk-key或者versionCode等】
+     */
+    Map<String, Object> mParams;
+    /**
+     * 是否使用的是Get请求
+     */
+    boolean mIsGet;
+    /**
+     * 是否只在wifi下进行版本更新检查
+     */
+    boolean mIsWifiOnly;
+    /**
+     * 是否是自动版本更新模式【无人干预,有版本更新直接下载、安装】
+     */
+    boolean mIsAutoMode;
+    /**
+     * 下载的apk文件缓存目录
+     */
+    String mApkCacheDir;
+    //========全局更新实现接口==========//
+    /**
+     * 版本更新网络请求服务API
+     */
+    IUpdateHttpService mUpdateHttpService;
+    /**
+     * 版本更新检查器【有默认】
+     */
+    IUpdateChecker mUpdateChecker;
+    /**
+     * 版本更新解析器【有默认】
+     */
+    IUpdateParser mUpdateParser;
+    /**
+     * 版本更新提示器【有默认】
+     */
+    IUpdatePrompter mUpdatePrompter;
+    /**
+     * 版本更新下载器【有默认】
+     */
+    IUpdateDownloader mUpdateDownloader;
+    /**
+     * 文件加密器【有默认】
+     */
+    IFileEncryptor mFileEncryptor;
+    /**
+     * APK安装监听【有默认】
+     */
+    OnInstallListener mOnInstallListener;
+    /**
+     * 更新出错监听【有默认】
+     */
+    OnUpdateFailureListener mOnUpdateFailureListener;
+
+    //===========================初始化===================================//
+
+    private XUpdate() {
+        mIsGet = false;
+        mIsWifiOnly = true;
+        mIsAutoMode = false;
+
+        mUpdateChecker = new DefaultUpdateChecker();
+        mUpdateParser = new DefaultUpdateParser();
+        mUpdateDownloader = new DefaultUpdateDownloader();
+        mUpdatePrompter = new DefaultUpdatePrompter();
+        mFileEncryptor = new DefaultFileEncryptor();
+        mOnInstallListener = new DefaultInstallListener();
+        mOnUpdateFailureListener = new DefaultUpdateFailureListener();
+    }
+
+    /**
+     * 获取版本更新的入口
+     *
+     * @return 版本更新的入口
+     */
+    public static XUpdate get() {
+        if (sInstance == null) {
+            synchronized (XUpdate.class) {
+                if (sInstance == null) {
+                    sInstance = new XUpdate();
+                }
+            }
+        }
+        return sInstance;
+    }
+
+    /**
+     * 初始化
+     *
+     * @param application 应用上下文
+     */
+    public void init(Application application) {
+        mContext = application;
+        UpdateError.init(mContext);
+    }
+
+    private Application getApplication() {
+        testInitialize();
+        return mContext;
+    }
+
+    private void testInitialize() {
+        if (mContext == null) {
+            throw new ExceptionInInitializerError("请先在全局Application中调用 XUpdate.get().init() 初始化!");
+        }
+    }
+
+    public static Context getContext() {
+        return get().getApplication();
+    }
+
+    //===========================对外版本更新api===================================//
+
+    /**
+     * 获取版本更新构建者
+     *
+     * @param context 上下文
+     * @return 版本更新构建者
+     */
+    public static UpdateManager.Builder newBuild(@NonNull Context context) {
+        return new UpdateManager.Builder(context);
+    }
+
+    /**
+     * 获取版本更新构建者
+     *
+     * @param context   上下文
+     * @param updateUrl 版本更新检查的地址
+     * @return 版本更新构建者
+     */
+    public static UpdateManager.Builder newBuild(@NonNull Context context, String updateUrl) {
+        return new UpdateManager.Builder(context)
+                .updateUrl(updateUrl);
+    }
+
+    //===========================属性设置===================================//
+
+    /**
+     * 设置全局的apk更新请求参数
+     *
+     * @param key   键
+     * @param value 值
+     * @return this
+     */
+    public XUpdate param(@NonNull String key, @NonNull Object value) {
+        if (mParams == null) {
+            mParams = new TreeMap<>();
+        }
+        UpdateLog.d("设置全局参数, key:" + key + ", value:" + value.toString());
+        mParams.put(key, value);
+        return this;
+    }
+
+    /**
+     * 设置全局的apk更新请求参数
+     *
+     * @param params apk更新请求参数
+     * @return this
+     */
+    public XUpdate params(@NonNull Map<String, Object> params) {
+        logForParams(params);
+        mParams = params;
+        return this;
+    }
+
+    private void logForParams(@NonNull Map<String, Object> params) {
+        StringBuilder sb = new StringBuilder("设置全局参数:{\n");
+        for (Map.Entry<String, Object> entry : params.entrySet()) {
+            sb.append("key = ")
+                    .append(entry.getKey())
+                    .append(", value = ")
+                    .append(entry.getValue().toString())
+                    .append("\n");
+        }
+        sb.append("}");
+        UpdateLog.d(sb.toString());
+    }
+
+
+    /**
+     * 设置全局版本更新网络请求服务API
+     *
+     * @param updateHttpService 版本更新网络请求服务API
+     * @return this
+     */
+    public XUpdate setIUpdateHttpService(@NonNull IUpdateHttpService updateHttpService) {
+        UpdateLog.d("设置全局更新网络请求服务:" + updateHttpService.getClass().getCanonicalName());
+        mUpdateHttpService = updateHttpService;
+        return this;
+    }
+
+    /**
+     * 设置全局版本更新检查
+     *
+     * @param updateChecker 版本更新检查器
+     * @return this
+     */
+    public XUpdate setIUpdateChecker(@NonNull IUpdateChecker updateChecker) {
+        mUpdateChecker = updateChecker;
+        return this;
+    }
+
+    /**
+     * 设置全局版本更新的解析器
+     *
+     * @param updateParser 版本更新的解析器
+     * @return this
+     */
+    public XUpdate setIUpdateParser(@NonNull IUpdateParser updateParser) {
+        mUpdateParser = updateParser;
+        return this;
+    }
+
+    /**
+     * 设置全局版本更新提示器
+     *
+     * @param updatePrompter 版本更新提示器
+     * @return this
+     */
+    public XUpdate setIUpdatePrompter(IUpdatePrompter updatePrompter) {
+        mUpdatePrompter = updatePrompter;
+        return this;
+    }
+
+    /**
+     * 设置全局版本更新下载器
+     *
+     * @param updateDownLoader 版本更新下载器
+     * @return this
+     */
+    public XUpdate setIUpdateDownLoader(@NonNull IUpdateDownloader updateDownLoader) {
+        mUpdateDownloader = updateDownLoader;
+        return this;
+    }
+
+    /**
+     * 设置是否使用的是Get请求
+     *
+     * @param isGet 是否使用的是Get请求
+     * @return this
+     */
+    public XUpdate isGet(boolean isGet) {
+        UpdateLog.d("设置全局是否使用的是Get请求:" + isGet);
+        mIsGet = isGet;
+        return this;
+    }
+
+    /**
+     * 设置是否只在wifi下进行版本更新检查
+     *
+     * @param isWifiOnly 是否只在wifi下进行版本更新检查
+     * @return this
+     */
+    public XUpdate isWifiOnly(boolean isWifiOnly) {
+        UpdateLog.d("设置全局是否只在wifi下进行版本更新检查:" + isWifiOnly);
+        mIsWifiOnly = isWifiOnly;
+        return this;
+    }
+
+    /**
+     * 设置是否是自动版本更新模式【无人干预,有版本更新直接下载、安装】
+     *
+     * @param isAutoMode 是否是自动版本更新模式
+     * @return this
+     */
+    public XUpdate isAutoMode(boolean isAutoMode) {
+        UpdateLog.d("设置全局是否是自动版本更新模式:" + isAutoMode);
+        mIsAutoMode = isAutoMode;
+        return this;
+    }
+
+    /**
+     * 设置apk的缓存路径
+     *
+     * @param apkCacheDir apk的缓存路径
+     * @return this
+     */
+    public XUpdate setApkCacheDir(String apkCacheDir) {
+        UpdateLog.d("设置全局apk的缓存路径:" + apkCacheDir);
+        mApkCacheDir = apkCacheDir;
+        return this;
+    }
+
+    /**
+     * 设置是否支持静默安装
+     *
+     * @param supportSilentInstall 是否支持静默安装
+     * @return this
+     */
+    public XUpdate supportSilentInstall(boolean supportSilentInstall) {
+        ApkInstallUtils.setSupportSilentInstall(supportSilentInstall);
+        return this;
+    }
+
+    /**
+     * 设置是否是debug模式
+     *
+     * @param isDebug 是否是debug模式
+     * @return this
+     */
+    public XUpdate debug(boolean isDebug) {
+        UpdateLog.debug(isDebug);
+        return this;
+    }
+
+    /**
+     * 设置日志打印接口
+     *
+     * @param logger 日志打印接口
+     * @return this
+     */
+    public XUpdate setILogger(@NonNull ILogger logger) {
+        UpdateLog.setLogger(logger);
+        return this;
+    }
+
+    //===========================apk安装监听===================================//
+
+
+    /**
+     * 设置文件加密器
+     *
+     * @param fileEncryptor 文件加密器
+     * @return this
+     */
+    public XUpdate setIFileEncryptor(IFileEncryptor fileEncryptor) {
+        mFileEncryptor = fileEncryptor;
+        return this;
+    }
+
+    /**
+     * 设置安装监听
+     *
+     * @param onInstallListener 安装监听
+     * @return this
+     */
+    public XUpdate setOnInstallListener(OnInstallListener onInstallListener) {
+        mOnInstallListener = onInstallListener;
+        return this;
+    }
+
+    //===========================更新出错===================================//
+
+    /**
+     * 设置更新出错的监听
+     *
+     * @param onUpdateFailureListener 更新出错的监听
+     * @return this
+     */
+    public XUpdate setOnUpdateFailureListener(@NonNull OnUpdateFailureListener onUpdateFailureListener) {
+        mOnUpdateFailureListener = onUpdateFailureListener;
+        return this;
+    }
+
+
+}

+ 336 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/_XUpdate.java

@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.LruCache;
+
+import com.xuexiang.xupdate.entity.DownloadEntity;
+import com.xuexiang.xupdate.entity.UpdateError;
+import com.xuexiang.xupdate.listener.OnInstallListener;
+import com.xuexiang.xupdate.listener.OnUpdateFailureListener;
+import com.xuexiang.xupdate.listener.impl.DefaultInstallListener;
+import com.xuexiang.xupdate.listener.impl.DefaultUpdateFailureListener;
+import com.xuexiang.xupdate.logs.UpdateLog;
+import com.xuexiang.xupdate.proxy.IUpdateChecker;
+import com.xuexiang.xupdate.proxy.IUpdateDownloader;
+import com.xuexiang.xupdate.proxy.IUpdateHttpService;
+import com.xuexiang.xupdate.proxy.IUpdateParser;
+import com.xuexiang.xupdate.proxy.IUpdatePrompter;
+import com.xuexiang.xupdate.proxy.impl.DefaultFileEncryptor;
+import com.xuexiang.xupdate.utils.ApkInstallUtils;
+
+import java.io.File;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+
+import androidx.annotation.NonNull;
+
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.INSTALL_FAILED;
+
+/**
+ * 内部版本更新参数的获取
+ *
+ * @author xuexiang
+ * @since 2018/7/10 下午4:27
+ */
+public final class _XUpdate {
+
+    /**
+     * 存储正在进行检查版本的状态,key为url,value为是否正在检查
+     */
+    private static final Map<String, Boolean> sCheckMap = new ConcurrentHashMap<>();
+    /**
+     * 存储是否正在显示版本更新,key为url,value为是否正在显示版本更新
+     */
+    private static final Map<String, Boolean> sPrompterMap = new ConcurrentHashMap<>();
+    /**
+     * Runnable等待队列
+     */
+    private static final Map<String, Runnable> sWaitRunnableMap = new ConcurrentHashMap<>();
+
+    /**
+     * 存储顶部图片资源
+     */
+    private static final LruCache<String, Drawable> sTopDrawableCache = new LruCache<>(4);
+
+    private static final Handler sMainHandler = new Handler(Looper.getMainLooper());
+
+    /**
+     * 10秒的检查延迟
+     */
+    private static final long CHECK_TIMEOUT = 10 * 1000L;
+
+    /**
+     * 设置版本检查的状态【防止重复检查】
+     *
+     * @param url        请求地址
+     * @param isChecking 是否正在检查
+     */
+    public static void setCheckUrlStatus(final String url, boolean isChecking) {
+        if (TextUtils.isEmpty(url)) {
+            return;
+        }
+        sCheckMap.put(url, isChecking);
+        Runnable waitRunnable = sWaitRunnableMap.get(url);
+        if (waitRunnable != null) {
+            sMainHandler.removeCallbacks(waitRunnable);
+            sWaitRunnableMap.remove(url);
+        }
+        if (isChecking) {
+            Runnable newRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    // 处理超时情况
+                    sWaitRunnableMap.remove(url);
+                    sCheckMap.put(url, false);
+                }
+            };
+            sMainHandler.postDelayed(newRunnable, CHECK_TIMEOUT);
+            sWaitRunnableMap.put(url, newRunnable);
+        }
+    }
+
+    /**
+     * 获取版本检查的状态
+     *
+     * @param url 请求地址
+     * @return 是否正在检查
+     */
+    public static boolean getCheckUrlStatus(String url) {
+        Boolean checkStatus = sCheckMap.get(url);
+        return checkStatus != null && checkStatus;
+    }
+
+    /**
+     * 设置版本更新弹窗是否已经显示
+     *
+     * @param url    请求地址
+     * @param isShow 是否已经显示
+     */
+    public static void setIsPrompterShow(String url, boolean isShow) {
+        if (TextUtils.isEmpty(url)) {
+            return;
+        }
+        sPrompterMap.put(url, isShow);
+    }
+
+    /**
+     * 获取版本更新弹窗是否已经显示
+     *
+     * @param url 请求地址
+     * @return 是否正在显示
+     */
+    public static boolean isPrompterShow(String url) {
+        Boolean isShow = sPrompterMap.get(url);
+        return isShow != null && isShow;
+    }
+
+    /**
+     * 保存顶部背景图片
+     *
+     * @param drawable 图片
+     * @return 图片标识
+     */
+    public static String saveTopDrawable(Drawable drawable) {
+        String tag = UUID.randomUUID().toString();
+        sTopDrawableCache.put(tag, drawable);
+        return tag;
+    }
+
+    /**
+     * 获取顶部背景图片
+     *
+     * @param drawableTag 图片标识
+     * @return 顶部背景图片
+     */
+    public static Drawable getTopDrawable(String drawableTag) {
+        if (TextUtils.isEmpty(drawableTag)) {
+            return null;
+        }
+        return sTopDrawableCache.get(drawableTag);
+    }
+
+    //===========================属性设置===================================//
+
+    public static Map<String, Object> getParams() {
+        return XUpdate.get().mParams;
+    }
+
+    public static IUpdateHttpService getIUpdateHttpService() {
+        return XUpdate.get().mUpdateHttpService;
+    }
+
+    public static IUpdateChecker getIUpdateChecker() {
+        return XUpdate.get().mUpdateChecker;
+    }
+
+    public static IUpdateParser getIUpdateParser() {
+        return XUpdate.get().mUpdateParser;
+    }
+
+    public static IUpdatePrompter getIUpdatePrompter() {
+        return XUpdate.get().mUpdatePrompter;
+    }
+
+    public static IUpdateDownloader getIUpdateDownLoader() {
+        return XUpdate.get().mUpdateDownloader;
+    }
+
+    public static boolean isGet() {
+        return XUpdate.get().mIsGet;
+    }
+
+    public static boolean isWifiOnly() {
+        return XUpdate.get().mIsWifiOnly;
+    }
+
+    public static boolean isAutoMode() {
+        return XUpdate.get().mIsAutoMode;
+    }
+
+    public static String getApkCacheDir() {
+        return XUpdate.get().mApkCacheDir;
+    }
+
+    //===========================文件加密===================================//
+
+    /**
+     * 加密文件
+     *
+     * @param file 需要加密的文件
+     */
+    public static String encryptFile(File file) {
+        if (XUpdate.get().mFileEncryptor == null) {
+            XUpdate.get().mFileEncryptor = new DefaultFileEncryptor();
+        }
+        return XUpdate.get().mFileEncryptor.encryptFile(file);
+    }
+
+    /**
+     * 验证文件是否有效(加密是否一致)
+     *
+     * @param encrypt 加密值,不能为空
+     * @param file    需要校验的文件
+     * @return 文件是否有效
+     */
+    public static boolean isFileValid(String encrypt, File file) {
+        if (XUpdate.get().mFileEncryptor == null) {
+            XUpdate.get().mFileEncryptor = new DefaultFileEncryptor();
+        }
+        return XUpdate.get().mFileEncryptor.isFileValid(encrypt, file);
+    }
+
+    //===========================apk安装监听===================================//
+
+    public static OnInstallListener getOnInstallListener() {
+        return XUpdate.get().mOnInstallListener;
+    }
+
+    /**
+     * 开始安装apk文件
+     *
+     * @param context 传activity可以获取安装的返回值,详见{@link ApkInstallUtils#REQUEST_CODE_INSTALL_APP}
+     * @param apkFile apk文件
+     */
+    public static void startInstallApk(@NonNull Context context, @NonNull File apkFile) {
+        startInstallApk(context, apkFile, new DownloadEntity());
+    }
+
+    /**
+     * 开始安装apk文件
+     *
+     * @param context        传activity可以获取安装的返回值,详见{@link ApkInstallUtils#REQUEST_CODE_INSTALL_APP}
+     * @param apkFile        apk文件
+     * @param downloadEntity 文件下载信息
+     */
+    public static void startInstallApk(@NonNull Context context, @NonNull File apkFile, @NonNull DownloadEntity downloadEntity) {
+        UpdateLog.d("开始安装apk文件, 文件路径:" + apkFile.getAbsolutePath() + ", 下载信息:" + downloadEntity);
+        if (onInstallApk(context, apkFile, downloadEntity)) {
+            onApkInstallSuccess(); //静默安装的话,不会回调到这里
+        } else {
+            onUpdateError(INSTALL_FAILED);
+        }
+    }
+
+    /**
+     * 安装apk
+     *
+     * @param context        传activity可以获取安装的返回值,详见{@link ApkInstallUtils#REQUEST_CODE_INSTALL_APP}
+     * @param apkFile        apk文件
+     * @param downloadEntity 文件下载信息
+     */
+    private static boolean onInstallApk(Context context, File apkFile, DownloadEntity downloadEntity) {
+        if (XUpdate.get().mOnInstallListener == null) {
+            XUpdate.get().mOnInstallListener = new DefaultInstallListener();
+        }
+        return XUpdate.get().mOnInstallListener.onInstallApk(context, apkFile, downloadEntity);
+    }
+
+    /**
+     * apk安装完毕
+     */
+    private static void onApkInstallSuccess() {
+        if (XUpdate.get().mOnInstallListener == null) {
+            XUpdate.get().mOnInstallListener = new DefaultInstallListener();
+        }
+        XUpdate.get().mOnInstallListener.onInstallApkSuccess();
+    }
+
+    //===========================更新出错===================================//
+
+    public static OnUpdateFailureListener getOnUpdateFailureListener() {
+        return XUpdate.get().mOnUpdateFailureListener;
+    }
+
+    /**
+     * 更新出现错误
+     *
+     * @param errorCode
+     */
+    public static void onUpdateError(int errorCode) {
+        onUpdateError(new UpdateError(errorCode));
+    }
+
+    /**
+     * 更新出现错误
+     *
+     * @param errorCode 错误码
+     * @param message   错误信息
+     */
+    public static void onUpdateError(int errorCode, String message) {
+        onUpdateError(new UpdateError(errorCode, message));
+    }
+
+    /**
+     * 更新出现错误
+     *
+     * @param updateError
+     */
+    public static void onUpdateError(@NonNull UpdateError updateError) {
+        if (XUpdate.get().mOnUpdateFailureListener == null) {
+            XUpdate.get().mOnUpdateFailureListener = new DefaultUpdateFailureListener();
+        }
+        XUpdate.get().mOnUpdateFailureListener.onFailure(updateError);
+    }
+
+}

+ 158 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/entity/DownloadEntity.java

@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.entity;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.xuexiang.xupdate._XUpdate;
+
+import java.io.File;
+
+/**
+ * 下载信息实体
+ *
+ * @author xuexiang
+ * @since 2018/7/9 上午11:41
+ */
+public class DownloadEntity implements Parcelable {
+    /**
+     * 下载地址
+     */
+    private String mDownloadUrl;
+    /**
+     * 文件下载的目录
+     */
+    private String mCacheDir;
+    /**
+     * 下载文件的加密值,用于校验,防止下载的apk文件被替换【当然你也可以不使用MD5加密】
+     */
+    private String mMd5;
+    /**
+     * 下载文件的大小【单位:KB】
+     */
+    private long mSize;
+    //==========================//
+    /**
+     * 是否在通知栏上显示下载进度
+     */
+    private boolean mIsShowNotification;
+
+    public DownloadEntity() {
+
+    }
+
+    protected DownloadEntity(Parcel in) {
+        mDownloadUrl = in.readString();
+        mCacheDir = in.readString();
+        mMd5 = in.readString();
+        mSize = in.readLong();
+        mIsShowNotification = in.readByte() != 0;
+    }
+
+    public static final Creator<DownloadEntity> CREATOR = new Creator<DownloadEntity>() {
+        @Override
+        public DownloadEntity createFromParcel(Parcel in) {
+            return new DownloadEntity(in);
+        }
+
+        @Override
+        public DownloadEntity[] newArray(int size) {
+            return new DownloadEntity[size];
+        }
+    };
+
+    public String getDownloadUrl() {
+        return mDownloadUrl;
+    }
+
+    public DownloadEntity setDownloadUrl(String downloadUrl) {
+        mDownloadUrl = downloadUrl;
+        return this;
+    }
+
+    public String getCacheDir() {
+        return mCacheDir;
+    }
+
+    public DownloadEntity setCacheDir(String cacheDir) {
+        mCacheDir = cacheDir;
+        return this;
+    }
+
+    public String getMd5() {
+        return mMd5;
+    }
+
+    public DownloadEntity setMd5(String md5) {
+        mMd5 = md5;
+        return this;
+    }
+
+    public long getSize() {
+        return mSize;
+    }
+
+    public DownloadEntity setSize(long size) {
+        mSize = size;
+        return this;
+    }
+
+    public boolean isShowNotification() {
+        return mIsShowNotification;
+    }
+
+    public DownloadEntity setShowNotification(boolean showNotification) {
+        mIsShowNotification = showNotification;
+        return this;
+    }
+
+    /**
+     * 验证文件是否有效【没设置mMd5默认不校验,直接有效】
+     *
+     * @param apkFile 需要校验的文件
+     * @return 文件是否有效
+     */
+    public boolean isApkFileValid(File apkFile) {
+        return _XUpdate.isFileValid(mMd5, apkFile);
+    }
+
+    @Override
+    public String toString() {
+        return "DownloadEntity{" +
+                "mDownloadUrl='" + mDownloadUrl + '\'' +
+                ", mCacheDir='" + mCacheDir + '\'' +
+                ", mMd5='" + mMd5 + '\'' +
+                ", mSize=" + mSize +
+                ", mIsShowNotification=" + mIsShowNotification +
+                '}';
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mDownloadUrl);
+        dest.writeString(mCacheDir);
+        dest.writeString(mMd5);
+        dest.writeLong(mSize);
+        dest.writeByte((byte) (mIsShowNotification ? 1 : 0));
+    }
+}

+ 189 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/entity/PromptEntity.java

@@ -0,0 +1,189 @@
+package com.xuexiang.xupdate.entity;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+
+/**
+ * 版本更新提示器参数信息
+ *
+ * @author xuexiang
+ * @since 2018/11/19 上午9:44
+ */
+public class PromptEntity implements Parcelable {
+
+    /**
+     * 主题颜色
+     */
+    @ColorInt
+    private int mThemeColor;
+    /**
+     * 顶部背景图片
+     */
+    @DrawableRes
+    private int mTopResId;
+    /**
+     * 顶部背景图片Drawable标识
+     */
+    private String mTopDrawableTag;
+    /**
+     * 按钮文字颜色
+     */
+    @ColorInt
+    private int mButtonTextColor;
+    /**
+     * 是否支持后台更新
+     */
+    private boolean mSupportBackgroundUpdate;
+    /**
+     * 版本更新提示器宽度占屏幕的比例
+     */
+    private float mWidthRatio;
+    /**
+     * 版本更新提示器高度占屏幕的比例
+     */
+    private float mHeightRatio;
+    /**
+     * 是否忽略下载异常【为true时,下载失败更新提示框不消失,默认是false】
+     */
+    private boolean mIgnoreDownloadError;
+
+    public PromptEntity() {
+        mThemeColor = -1;
+        mTopResId = -1;
+        mTopDrawableTag = "";
+        mButtonTextColor = 0;
+        mSupportBackgroundUpdate = false;
+        mWidthRatio = -1;
+        mHeightRatio = -1;
+        mIgnoreDownloadError = false;
+    }
+
+    protected PromptEntity(Parcel in) {
+        mThemeColor = in.readInt();
+        mTopResId = in.readInt();
+        mTopDrawableTag = in.readString();
+        mButtonTextColor = in.readInt();
+        mSupportBackgroundUpdate = in.readByte() != 0;
+        mWidthRatio = in.readFloat();
+        mHeightRatio = in.readFloat();
+        mIgnoreDownloadError = in.readByte() != 0;
+    }
+
+    public static final Creator<PromptEntity> CREATOR = new Creator<PromptEntity>() {
+        @Override
+        public PromptEntity createFromParcel(Parcel in) {
+            return new PromptEntity(in);
+        }
+
+        @Override
+        public PromptEntity[] newArray(int size) {
+            return new PromptEntity[size];
+        }
+    };
+
+    public int getThemeColor() {
+        return mThemeColor;
+    }
+
+    public PromptEntity setThemeColor(int themeColor) {
+        mThemeColor = themeColor;
+        return this;
+    }
+
+    public int getTopResId() {
+        return mTopResId;
+    }
+
+    public PromptEntity setTopResId(int topResId) {
+        mTopResId = topResId;
+        return this;
+    }
+
+    public String getTopDrawableTag() {
+        return mTopDrawableTag;
+    }
+
+    public PromptEntity setTopDrawableTag(String topDrawableTag) {
+        mTopDrawableTag = topDrawableTag;
+        return this;
+    }
+
+    public int getButtonTextColor() {
+        return mButtonTextColor;
+    }
+
+    public PromptEntity setButtonTextColor(int buttonTextColor) {
+        mButtonTextColor = buttonTextColor;
+        return this;
+    }
+
+    public boolean isSupportBackgroundUpdate() {
+        return mSupportBackgroundUpdate;
+    }
+
+    public PromptEntity setSupportBackgroundUpdate(boolean supportBackgroundUpdate) {
+        mSupportBackgroundUpdate = supportBackgroundUpdate;
+        return this;
+    }
+
+    public PromptEntity setWidthRatio(float widthRatio) {
+        mWidthRatio = widthRatio;
+        return this;
+    }
+
+    public float getWidthRatio() {
+        return mWidthRatio;
+    }
+
+    public PromptEntity setHeightRatio(float heightRatio) {
+        mHeightRatio = heightRatio;
+        return this;
+    }
+
+    public float getHeightRatio() {
+        return mHeightRatio;
+    }
+
+    public PromptEntity setIgnoreDownloadError(boolean ignoreDownloadError) {
+        mIgnoreDownloadError = ignoreDownloadError;
+        return this;
+    }
+
+    public boolean isIgnoreDownloadError() {
+        return mIgnoreDownloadError;
+    }
+
+    @Override
+    public String toString() {
+        return "PromptEntity{" +
+                "mThemeColor=" + mThemeColor +
+                ", mTopResId=" + mTopResId +
+                ", mTopDrawableTag=" + mTopDrawableTag +
+                ", mButtonTextColor=" + mButtonTextColor +
+                ", mSupportBackgroundUpdate=" + mSupportBackgroundUpdate +
+                ", mWidthRatio=" + mWidthRatio +
+                ", mHeightRatio=" + mHeightRatio +
+                ", mIgnoreDownloadError=" + mIgnoreDownloadError +
+                '}';
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mThemeColor);
+        dest.writeInt(mTopResId);
+        dest.writeString(mTopDrawableTag);
+        dest.writeInt(mButtonTextColor);
+        dest.writeByte((byte) (mSupportBackgroundUpdate ? 1 : 0));
+        dest.writeFloat(mWidthRatio);
+        dest.writeFloat(mHeightRatio);
+        dest.writeByte((byte) (mIgnoreDownloadError ? 1 : 0));
+    }
+}

+ 320 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/entity/UpdateEntity.java

@@ -0,0 +1,320 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.entity;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import com.xuexiang.xupdate.proxy.IUpdateHttpService;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * 版本更新信息实体
+ *
+ * @author xuexiang
+ * @since 2018/6/29 下午9:33
+ */
+public class UpdateEntity implements Parcelable {
+    //===========是否可以升级=============//
+    /**
+     * 是否有新版本
+     */
+    private boolean mHasUpdate;
+
+    /**
+     * 是否强制安装:不安装无法使用app
+     */
+    private boolean mIsForce;
+
+    /**
+     * 是否可忽略该版本
+     */
+    private boolean mIsIgnorable;
+
+    //===========升级的信息=============//
+    /**
+     * 版本号
+     */
+    private int mVersionCode;
+    /**
+     * 版本名称
+     */
+    private String mVersionName;
+
+    /**
+     * 更新内容
+     */
+    private String mUpdateContent;
+
+    /**
+     * 下载信息实体
+     */
+    private DownloadEntity mDownloadEntity;
+
+    //============升级行为============//
+    /**
+     * 是否静默下载:有新版本时不提示直接下载
+     */
+    private boolean mIsSilent;
+    /**
+     * 是否下载完成后自动安装[默认是true]
+     */
+    private boolean mIsAutoInstall;
+
+    public UpdateEntity() {
+        mVersionName = "unknown_version";
+        mDownloadEntity = new DownloadEntity();
+        mIsAutoInstall = true;
+    }
+
+    protected UpdateEntity(Parcel in) {
+        mHasUpdate = in.readByte() != 0;
+        mIsForce = in.readByte() != 0;
+        mIsIgnorable = in.readByte() != 0;
+        mVersionCode = in.readInt();
+        mVersionName = in.readString();
+        mUpdateContent = in.readString();
+        mDownloadEntity = in.readParcelable(DownloadEntity.class.getClassLoader());
+        mIsSilent = in.readByte() != 0;
+        mIsAutoInstall = in.readByte() != 0;
+    }
+
+    public static final Creator<UpdateEntity> CREATOR = new Creator<UpdateEntity>() {
+        @Override
+        public UpdateEntity createFromParcel(Parcel in) {
+            return new UpdateEntity(in);
+        }
+
+        @Override
+        public UpdateEntity[] newArray(int size) {
+            return new UpdateEntity[size];
+        }
+    };
+
+    public boolean isHasUpdate() {
+        return mHasUpdate;
+    }
+
+    public UpdateEntity setHasUpdate(boolean hasUpdate) {
+        mHasUpdate = hasUpdate;
+        return this;
+    }
+
+    public boolean isForce() {
+        return mIsForce;
+    }
+
+    public UpdateEntity setForce(boolean force) {
+        if (force) {
+            //强制更新,不可以忽略
+            mIsIgnorable = false;
+        }
+        mIsForce = force;
+        return this;
+    }
+
+    public boolean isIgnorable() {
+        return mIsIgnorable;
+    }
+
+    public UpdateEntity setIsIgnorable(boolean isIgnorable) {
+        if (isIgnorable) {
+            //可忽略的,不能是强制更新
+            mIsForce = false;
+        }
+        mIsIgnorable = isIgnorable;
+        return this;
+    }
+
+    public boolean isSilent() {
+        return mIsSilent;
+    }
+
+    public UpdateEntity setIsSilent(boolean isSilent) {
+        mIsSilent = isSilent;
+        return this;
+    }
+
+    public boolean isAutoInstall() {
+        return mIsAutoInstall;
+    }
+
+    public UpdateEntity setIsAutoInstall(boolean isAutoInstall) {
+        mIsAutoInstall = isAutoInstall;
+        return this;
+    }
+
+    /**
+     * 设置apk的缓存地址,只支持设置一次
+     *
+     * @param apkCacheDir
+     * @return
+     */
+    public UpdateEntity setApkCacheDir(String apkCacheDir) {
+        if (!TextUtils.isEmpty(apkCacheDir) && TextUtils.isEmpty(mDownloadEntity.getCacheDir())) {
+            mDownloadEntity.setCacheDir(apkCacheDir);
+        }
+        return this;
+    }
+
+    /**
+     * 设置是否是自动模式【自动静默下载,自动安装】
+     *
+     * @param isAutoMode
+     */
+    public UpdateEntity setIsAutoMode(boolean isAutoMode) {
+        if (isAutoMode) {
+            //自动下载
+            mIsSilent = true;
+            //自动安装
+            mIsAutoInstall = true;
+            //自动模式下,默认下载进度条在通知栏显示
+            mDownloadEntity.setShowNotification(true);
+        }
+        return this;
+    }
+
+    /**
+     * 设置是否显示下载通知
+     *
+     * @param showNotification 是否显示下载通知
+     * @return
+     */
+    public UpdateEntity setShowNotification(boolean showNotification) {
+        mDownloadEntity.setShowNotification(showNotification);
+        return this;
+    }
+
+    public int getVersionCode() {
+        return mVersionCode;
+    }
+
+    public UpdateEntity setVersionCode(int versionCode) {
+        mVersionCode = versionCode;
+        return this;
+    }
+
+    public String getVersionName() {
+        return mVersionName;
+    }
+
+    public UpdateEntity setVersionName(String versionName) {
+        mVersionName = versionName;
+        return this;
+    }
+
+    public String getUpdateContent() {
+        return mUpdateContent;
+    }
+
+    public UpdateEntity setUpdateContent(String updateContent) {
+        mUpdateContent = updateContent;
+        return this;
+    }
+
+    public String getDownloadUrl() {
+        return mDownloadEntity.getDownloadUrl();
+    }
+
+    public UpdateEntity setDownloadUrl(String downloadUrl) {
+        mDownloadEntity.setDownloadUrl(downloadUrl);
+        return this;
+    }
+
+    public String getMd5() {
+        return mDownloadEntity.getMd5();
+    }
+
+    public UpdateEntity setMd5(String md5) {
+        mDownloadEntity.setMd5(md5);
+        return this;
+    }
+
+    public long getSize() {
+        return mDownloadEntity.getSize();
+    }
+
+    public UpdateEntity setSize(long size) {
+        mDownloadEntity.setSize(size);
+        return this;
+    }
+
+    public String getApkCacheDir() {
+        return mDownloadEntity.getCacheDir();
+    }
+
+    public UpdateEntity setDownLoadEntity(@NonNull DownloadEntity downloadEntity) {
+        mDownloadEntity = downloadEntity;
+        return this;
+    }
+
+    @NonNull
+    public DownloadEntity getDownLoadEntity() {
+        return mDownloadEntity;
+    }
+
+    //======内部变量,请勿设置=====//
+
+    private IUpdateHttpService mIUpdateHttpService;
+
+    public UpdateEntity setIUpdateHttpService(@NonNull IUpdateHttpService updateHttpService) {
+        mIUpdateHttpService = updateHttpService;
+        return this;
+    }
+
+    @Nullable
+    public IUpdateHttpService getIUpdateHttpService() {
+        return mIUpdateHttpService;
+    }
+
+    @Override
+    public String toString() {
+        return "UpdateEntity{" +
+                "mHasUpdate=" + mHasUpdate +
+                ", mIsForce=" + mIsForce +
+                ", mIsIgnorable=" + mIsIgnorable +
+                ", mVersionCode=" + mVersionCode +
+                ", mVersionName='" + mVersionName + '\'' +
+                ", mUpdateContent='" + mUpdateContent + '\'' +
+                ", mDownloadEntity=" + mDownloadEntity +
+                ", mIsSilent=" + mIsSilent +
+                ", mIsAutoInstall=" + mIsAutoInstall +
+                ", mIUpdateHttpService=" + mIUpdateHttpService +
+                '}';
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByte((byte) (mHasUpdate ? 1 : 0));
+        dest.writeByte((byte) (mIsForce ? 1 : 0));
+        dest.writeByte((byte) (mIsIgnorable ? 1 : 0));
+        dest.writeInt(mVersionCode);
+        dest.writeString(mVersionName);
+        dest.writeString(mUpdateContent);
+        dest.writeParcelable(mDownloadEntity, flags);
+        dest.writeByte((byte) (mIsSilent ? 1 : 0));
+        dest.writeByte((byte) (mIsAutoInstall ? 1 : 0));
+    }
+}

+ 143 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/entity/UpdateError.java

@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.entity;
+
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.SparseArray;
+
+import com.xuexiang.xupdate.R;
+
+/**
+ * 更新错误
+ *
+ * @author xuexiang
+ * @since 2018/6/29 下午9:01
+ */
+public class UpdateError extends Throwable {
+
+    /**
+     * 错误码
+     */
+    private final int mCode;
+
+    public UpdateError(int code) {
+        this(code, null);
+    }
+
+    public UpdateError(int code, String message) {
+        super(make(code, message));
+        mCode = code;
+    }
+
+    public UpdateError(Throwable e) {
+        super(e);
+        mCode = ERROR.UPDATE_UNKNOWN;
+    }
+
+    public int getCode() {
+        return mCode;
+    }
+
+    @Override
+    public String toString() {
+        return getMessage();
+    }
+
+    private static String make(int code, String message) {
+        String m = sMessages.get(code);
+        if (TextUtils.isEmpty(m)) {
+            return "";
+        }
+        if (TextUtils.isEmpty(message) || message.equals("null")) {
+            return m;
+        }
+        return m + "(" + message + ")";
+    }
+
+    /**
+     * 获取详细的错误信息
+     *
+     * @return
+     */
+    public String getDetailMsg() {
+        return "Code:" + mCode + ", msg:" + getMessage();
+    }
+
+    /**
+     * 版本更新错误码
+     */
+    public final static class ERROR {
+
+        /**
+         * 查询更新失败
+         */
+        public static final int CHECK_NET_REQUEST = 2000;
+        public static final int CHECK_NO_WIFI = CHECK_NET_REQUEST + 1;
+        public static final int CHECK_NO_NETWORK = CHECK_NO_WIFI + 1;
+        public static final int CHECK_UPDATING = CHECK_NO_NETWORK + 1;
+        public static final int CHECK_NO_NEW_VERSION = CHECK_UPDATING + 1;
+        public static final int CHECK_JSON_EMPTY = CHECK_NO_NEW_VERSION + 1;
+        public static final int CHECK_PARSE = CHECK_JSON_EMPTY + 1;
+        public static final int CHECK_IGNORED_VERSION = CHECK_PARSE + 1;
+        public static final int CHECK_APK_CACHE_DIR_EMPTY = CHECK_IGNORED_VERSION + 1;
+
+        public static final int PROMPT_UNKNOWN = 3000;
+        public static final int PROMPT_ACTIVITY_DESTROY = PROMPT_UNKNOWN + 1;
+
+        public static final int DOWNLOAD_FAILED = 4000;
+        public static final int DOWNLOAD_PERMISSION_DENIED = DOWNLOAD_FAILED + 1;
+
+        /**
+         * apk安装错误
+         */
+        public static final int INSTALL_FAILED = 5000;
+
+        /**
+         * 未知的错误
+         */
+        public static final int UPDATE_UNKNOWN = 5100;
+    }
+
+    private static final SparseArray<String> sMessages = new SparseArray<>();
+
+    /**
+     * 初始化错误信息
+     *
+     * @param context
+     */
+    public static void init(Context context) {
+        sMessages.append(ERROR.CHECK_NET_REQUEST, context.getString(R.string.xupdate_error_check_net_request));
+        sMessages.append(ERROR.CHECK_NO_WIFI, context.getString(R.string.xupdate_error_check_no_wifi));
+        sMessages.append(ERROR.CHECK_NO_NETWORK, context.getString(R.string.xupdate_error_check_no_network));
+        sMessages.append(ERROR.CHECK_UPDATING, context.getString(R.string.xupdate_error_check_updating));
+        sMessages.append(ERROR.CHECK_NO_NEW_VERSION, context.getString(R.string.xupdate_error_check_no_new_version));
+        sMessages.append(ERROR.CHECK_JSON_EMPTY, context.getString(R.string.xupdate_error_check_json_empty));
+        sMessages.append(ERROR.CHECK_PARSE, context.getString(R.string.xupdate_error_check_parse));
+        sMessages.append(ERROR.CHECK_IGNORED_VERSION, context.getString(R.string.xupdate_error_check_ignored_version));
+        sMessages.append(ERROR.CHECK_APK_CACHE_DIR_EMPTY, context.getString(R.string.xupdate_error_check_apk_cache_dir_empty));
+
+        sMessages.append(ERROR.PROMPT_UNKNOWN, context.getString(R.string.xupdate_error_prompt_unknown));
+        sMessages.append(ERROR.PROMPT_ACTIVITY_DESTROY, context.getString(R.string.xupdate_error_prompt_activity_destroy));
+
+        sMessages.append(ERROR.DOWNLOAD_FAILED, context.getString(R.string.xupdate_error_download_failed));
+        sMessages.append(ERROR.DOWNLOAD_PERMISSION_DENIED, context.getString(R.string.xupdate_error_download_permission_denied));
+
+        sMessages.append(ERROR.INSTALL_FAILED, context.getString(R.string.xupdate_error_install_failed));
+    }
+}

+ 20 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/listener/IUpdateParseCallback.java

@@ -0,0 +1,20 @@
+package com.xuexiang.xupdate.listener;
+
+import com.xuexiang.xupdate.entity.UpdateEntity;
+
+/**
+ * 异步解析的回调
+ *
+ * @author xuexiang
+ * @since 2020-02-15 17:23
+ */
+public interface IUpdateParseCallback {
+
+    /**
+     * 解析结果
+     *
+     * @param updateEntity 版本更新信息实体
+     */
+    void onParseResult(UpdateEntity updateEntity);
+
+}

+ 47 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/listener/OnInstallListener.java

@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.listener;
+
+import android.content.Context;
+
+import com.xuexiang.xupdate.entity.DownloadEntity;
+
+import java.io.File;
+
+import androidx.annotation.NonNull;
+
+/**
+ * 安装监听
+ *
+ * @author xuexiang
+ * @since 2018/6/29 下午4:14
+ */
+public interface OnInstallListener {
+
+    /**
+     * 开始安装apk的监听
+     *
+     * @param apkFile        安装的apk文件
+     * @param downloadEntity 文件下载信息
+     */
+    boolean onInstallApk(@NonNull Context context, @NonNull File apkFile, @NonNull DownloadEntity downloadEntity);
+
+    /**
+     * apk安装完毕
+     */
+    void onInstallApkSuccess();
+}

+ 35 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/listener/OnUpdateFailureListener.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.listener;
+
+
+import com.xuexiang.xupdate.entity.UpdateError;
+
+/**
+ * 更新失败监听
+ *
+ * @author xuexiang
+ * @since 2018/7/1 下午7:43
+ */
+public interface OnUpdateFailureListener {
+    /**
+     * 更新失败
+     *
+     * @param error 错误
+     */
+    void onFailure(UpdateError error);
+}

+ 83 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/listener/impl/DefaultInstallListener.java

@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.listener.impl;
+
+import android.content.Context;
+
+import com.xuexiang.xupdate._XUpdate;
+import com.xuexiang.xupdate.entity.DownloadEntity;
+import com.xuexiang.xupdate.listener.OnInstallListener;
+import com.xuexiang.xupdate.utils.ApkInstallUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+import androidx.annotation.NonNull;
+
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.INSTALL_FAILED;
+
+/**
+ * 默认的apk安装监听【自定义安装监听可继承该类,并重写相应的方法】
+ *
+ * @author xuexiang
+ * @since 2018/7/1 下午11:58
+ */
+public class DefaultInstallListener implements OnInstallListener {
+
+    @Override
+    public boolean onInstallApk(@NonNull Context context, @NonNull File apkFile, @NonNull DownloadEntity downloadEntity) {
+        if (checkApkFile(downloadEntity, apkFile)) {
+            return installApkFile(context, apkFile);
+        } else {
+            _XUpdate.onUpdateError(INSTALL_FAILED, "Apk file verify failed, please check whether the MD5 value you set is correct!");
+            return false;
+        }
+    }
+
+    /**
+     * 检验apk文件的有效性(默认是使用MD5进行校验,可重写该方法)
+     *
+     * @param downloadEntity 下载信息实体
+     * @param apkFile        apk文件
+     * @return apk文件是否有效
+     */
+    protected boolean checkApkFile(DownloadEntity downloadEntity, @NonNull File apkFile) {
+        return downloadEntity != null && downloadEntity.isApkFileValid(apkFile);
+    }
+
+    /**
+     * 安装apk文件【此处可自定义apk的安装方法,可重写该方法】
+     *
+     * @param context 上下文
+     * @param apkFile apk文件
+     * @return 是否安装成功
+     */
+    protected boolean installApkFile(Context context, File apkFile) {
+        try {
+            return ApkInstallUtils.install(context, apkFile);
+        } catch (IOException e) {
+            _XUpdate.onUpdateError(INSTALL_FAILED, "An error occurred while install apk:" + e.getMessage());
+        }
+        return false;
+    }
+
+
+    @Override
+    public void onInstallApkSuccess() {
+
+    }
+}

+ 35 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/listener/impl/DefaultUpdateFailureListener.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.listener.impl;
+
+import com.xuexiang.xupdate.entity.UpdateError;
+import com.xuexiang.xupdate.listener.OnUpdateFailureListener;
+import com.xuexiang.xupdate.logs.UpdateLog;
+
+/**
+ * 默认的更新出错的处理(简单地打印日志)
+ *
+ * @author xuexiang
+ * @since 2018/7/1 下午7:48
+ */
+public class DefaultUpdateFailureListener implements OnUpdateFailureListener {
+
+    @Override
+    public void onFailure(UpdateError error) {
+        UpdateLog.e(error);
+    }
+}

+ 37 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/logs/ILogger.java

@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.logs;
+
+/**
+ * 简易的日志接口
+ *
+ * @author xuexiang
+ * @since 2018/6/29 下午7:57
+ */
+public interface ILogger {
+
+    /**
+     * 打印信息
+     *
+     * @param priority 优先级
+     * @param tag      标签
+     * @param message  信息
+     * @param t        出错信息
+     */
+    void log(int priority, String tag, String message, Throwable t);
+
+}

+ 134 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/logs/LogcatLogger.java

@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.logs;
+
+import android.util.Log;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import androidx.annotation.NonNull;
+
+/**
+ * 默认Logcat日志记录
+ *
+ * @author xuexiang
+ * @since 2018/6/29 下午7:57
+ */
+public class LogcatLogger implements ILogger {
+
+    /**
+     * logcat里日志的最大长度.
+     */
+    private static final int MAX_LOG_LENGTH = 4000;
+
+    /**
+     * 打印信息
+     *
+     * @param priority 优先级
+     * @param tag      标签
+     * @param message   信息
+     * @param t        出错信息
+     */
+    @Override
+    public void log(int priority, String tag, String message, Throwable t) {
+        if (message != null && message.length() == 0) {
+            message = null;
+        }
+        if (message == null) {
+            if (t == null) {
+                return; // Swallow message if it's null and there's no throwable.
+            }
+            message = getStackTraceString(t);
+        } else {
+            if (t != null) {
+                message += "\n" + getStackTraceString(t);
+            }
+        }
+
+        log(priority, tag, message);
+    }
+
+    private String getStackTraceString(Throwable t) {
+        // Don't replace this with Log.getStackTraceString() - it hides
+        // UnknownHostException, which is not what we want.
+        StringWriter sw = new StringWriter(256);
+        PrintWriter pw = new PrintWriter(sw, false);
+        t.printStackTrace(pw);
+        pw.flush();
+        return sw.toString();
+    }
+
+
+    /**
+     * 使用LogCat输出日志,字符长度超过4000则自动换行.
+     *
+     * @param priority 优先级
+     * @param tag      标签
+     * @param message  信息
+     */
+    public void log(int priority, String tag, String message) {
+        int subNum = message.length() / MAX_LOG_LENGTH;
+        if (subNum > 0) {
+            int index = 0;
+            for (int i = 0; i < subNum; i++) {
+                int lastIndex = index + MAX_LOG_LENGTH;
+                String sub = message.substring(index, lastIndex);
+                logSub(priority, tag, sub);
+                index = lastIndex;
+            }
+            logSub(priority, tag, message.substring(index, message.length()));
+        } else {
+            logSub(priority, tag, message);
+        }
+    }
+
+
+    /**
+     * 使用LogCat输出日志.
+     *
+     * @param priority 优先级
+     * @param tag      标签
+     * @param sub      信息
+     */
+    private void logSub(int priority, @NonNull String tag, @NonNull String sub) {
+        switch (priority) {
+            case Log.VERBOSE:
+                Log.v(tag, sub);
+                break;
+            case Log.DEBUG:
+                Log.d(tag, sub);
+                break;
+            case Log.INFO:
+                Log.i(tag, sub);
+                break;
+            case Log.WARN:
+                Log.w(tag, sub);
+                break;
+            case Log.ERROR:
+                Log.e(tag, sub);
+                break;
+            case Log.ASSERT:
+                Log.wtf(tag, sub);
+                break;
+            default:
+                Log.v(tag, sub);
+                break;
+        }
+    }
+
+}

+ 337 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/logs/UpdateLog.java

@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.logs;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+/**
+ * 版本更新日志打印
+ *
+ * @author xuexiang
+ * @since 2018/6/29 下午7:58
+ */
+public final class UpdateLog {
+
+    /**
+     * Don't let anyone instantiate this class.
+     */
+    private UpdateLog() {
+        throw new UnsupportedOperationException("Do not need instantiate!");
+    }
+
+    //==============常量================//
+    /**
+     * 默认tag
+     */
+    public final static String DEFAULT_LOG_TAG = "[XUpdate]";
+    /**
+     * 最大日志优先级【日志优先级为最大等级,所有日志都不打印】
+     */
+    private final static int MAX_LOG_PRIORITY = 10;
+    /**
+     * 最小日志优先级【日志优先级为最小等级,所有日志都打印】
+     */
+    private final static int MIN_LOG_PRIORITY = 0;
+
+    //==============属性================//
+    /**
+     * 默认的日志记录为Logcat
+     */
+    private static ILogger sILogger = new LogcatLogger();
+
+    private static String sTag = DEFAULT_LOG_TAG;
+    /**
+     * 是否是调试模式
+     */
+    private static boolean sIsDebug = false;
+    /**
+     * 日志打印优先级
+     */
+    private static int sLogPriority = MAX_LOG_PRIORITY;
+
+    //==============属性设置================//
+
+    /**
+     * 设置日志记录者的接口
+     *
+     * @param logger
+     */
+    public static void setLogger(@NonNull ILogger logger) {
+        sILogger = logger;
+    }
+
+    /**
+     * 设置日志的tag
+     *
+     * @param tag
+     */
+    public static void setTag(String tag) {
+        sTag = tag;
+    }
+
+    /**
+     * 设置是否是调试模式
+     *
+     * @param isDebug
+     */
+    public static void setDebug(boolean isDebug) {
+        sIsDebug = isDebug;
+    }
+
+    /**
+     * 设置打印日志的等级(只打印改等级以上的日志)
+     *
+     * @param priority
+     */
+    public static void setPriority(int priority) {
+        sLogPriority = priority;
+    }
+
+    //===================对外接口=======================//
+
+    /**
+     * 设置是否打开调试
+     *
+     * @param isDebug
+     */
+    public static void debug(boolean isDebug) {
+        if (isDebug) {
+            debug(DEFAULT_LOG_TAG);
+        } else {
+            debug("");
+        }
+    }
+
+    /**
+     * 设置调试模式
+     *
+     * @param tag
+     */
+    public static void debug(String tag) {
+        if (!TextUtils.isEmpty(tag)) {
+            setDebug(true);
+            setPriority(MIN_LOG_PRIORITY);
+            setTag(tag);
+        } else {
+            setDebug(false);
+            setPriority(MAX_LOG_PRIORITY);
+            setTag("");
+        }
+    }
+
+    //=============打印方法===============//
+
+    /**
+     * 打印任何(所有)信息
+     *
+     * @param msg
+     */
+    public static void v(String msg) {
+        if (enableLog(Log.VERBOSE)) {
+            sILogger.log(Log.VERBOSE, sTag, msg, null);
+        }
+    }
+
+    /**
+     * 打印任何(所有)信息
+     *
+     * @param tag
+     * @param msg
+     */
+    public static void vTag(String tag, String msg) {
+        if (enableLog(Log.VERBOSE)) {
+            sILogger.log(Log.VERBOSE, tag, msg, null);
+        }
+    }
+
+    /**
+     * 打印调试信息
+     *
+     * @param msg
+     */
+    public static void d(String msg) {
+        if (enableLog(Log.DEBUG)) {
+            sILogger.log(Log.DEBUG, sTag, msg, null);
+        }
+    }
+
+    /**
+     * 打印调试信息
+     *
+     * @param tag
+     * @param msg
+     */
+    public static void dTag(String tag, String msg) {
+        if (enableLog(Log.DEBUG)) {
+            sILogger.log(Log.DEBUG, tag, msg, null);
+        }
+    }
+
+    /**
+     * 打印提示性的信息
+     *
+     * @param msg
+     */
+    public static void i(String msg) {
+        if (enableLog(Log.INFO)) {
+            sILogger.log(Log.INFO, sTag, msg, null);
+        }
+    }
+
+    /**
+     * 打印提示性的信息
+     *
+     * @param tag
+     * @param msg
+     */
+    public static void iTag(String tag, String msg) {
+        if (enableLog(Log.INFO)) {
+            sILogger.log(Log.INFO, tag, msg, null);
+        }
+    }
+
+    /**
+     * 打印warning警告信息
+     *
+     * @param msg
+     */
+    public static void w(String msg) {
+        if (enableLog(Log.WARN)) {
+            sILogger.log(Log.WARN, sTag, msg, null);
+        }
+    }
+
+    /**
+     * 打印warning警告信息
+     *
+     * @param tag
+     * @param msg
+     */
+    public static void wTag(String tag, String msg) {
+        if (enableLog(Log.WARN)) {
+            sILogger.log(Log.WARN, tag, msg, null);
+        }
+    }
+
+    /**
+     * 打印出错信息
+     *
+     * @param msg
+     */
+    public static void e(String msg) {
+        if (enableLog(Log.ERROR)) {
+            sILogger.log(Log.ERROR, sTag, msg, null);
+        }
+    }
+
+    /**
+     * 打印出错信息
+     *
+     * @param tag
+     * @param msg
+     */
+    public static void eTag(String tag, String msg) {
+        if (enableLog(Log.ERROR)) {
+            sILogger.log(Log.ERROR, tag, msg, null);
+        }
+    }
+
+    /**
+     * 打印出错堆栈信息
+     *
+     * @param t
+     */
+    public static void e(Throwable t) {
+        if (enableLog(Log.ERROR)) {
+            sILogger.log(Log.ERROR, sTag, null, t);
+        }
+    }
+
+    /**
+     * 打印出错堆栈信息
+     *
+     * @param tag
+     * @param t
+     */
+    public static void eTag(String tag, Throwable t) {
+        if (enableLog(Log.ERROR)) {
+            sILogger.log(Log.ERROR, tag, null, t);
+        }
+    }
+
+
+    /**
+     * 打印出错堆栈信息
+     *
+     * @param msg
+     * @param t
+     */
+    public static void e(String msg, Throwable t) {
+        if (enableLog(Log.ERROR)) {
+            sILogger.log(Log.ERROR, sTag, msg, t);
+        }
+    }
+
+    /**
+     * 打印出错堆栈信息
+     *
+     * @param tag
+     * @param msg
+     * @param t
+     */
+    public static void eTag(String tag, String msg, Throwable t) {
+        if (enableLog(Log.ERROR)) {
+            sILogger.log(Log.ERROR, tag, msg, t);
+        }
+    }
+
+    /**
+     * 打印严重的错误信息
+     *
+     * @param msg
+     */
+    public static void wtf(String msg) {
+        if (enableLog(Log.ASSERT)) {
+            sILogger.log(Log.ASSERT, sTag, msg, null);
+        }
+    }
+
+    /**
+     * 打印严重的错误信息
+     *
+     * @param tag
+     * @param msg
+     */
+    public static void wtfTag(String tag, String msg) {
+        if (enableLog(Log.ASSERT)) {
+            sILogger.log(Log.ASSERT, tag, msg, null);
+        }
+    }
+
+    /**
+     * 能否打印
+     *
+     * @param logPriority
+     * @return
+     */
+    private static boolean enableLog(int logPriority) {
+        return sILogger != null && sIsDebug && logPriority >= sLogPriority;
+    }
+}

+ 29 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IFileEncryptor.java

@@ -0,0 +1,29 @@
+package com.xuexiang.xupdate.proxy;
+
+import java.io.File;
+
+/**
+ * 文件加密器,用于文件有效性校验
+ *
+ * @author xuexiang
+ * @since 2019-09-06 14:15
+ */
+public interface IFileEncryptor {
+
+    /**
+     * 加密文件
+     *
+     * @param file
+     * @return
+     */
+    String encryptFile(File file);
+
+    /**
+     * 检验文件是否有效(加密是否一致)
+     *
+     * @param encrypt 加密值
+     * @param file    需要校验的文件
+     * @return 文件是否有效
+     */
+    boolean isFileValid(String encrypt, File file);
+}

+ 46 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IPrompterProxy.java

@@ -0,0 +1,46 @@
+package com.xuexiang.xupdate.proxy;
+
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.service.OnFileDownloadListener;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * 版本更新提示器代理
+ *
+ * @author xuexiang
+ * @since 2020/6/9 12:16 AM
+ */
+public interface IPrompterProxy {
+
+    /**
+     * 获取版本更新的地址
+     *
+     * @return 版本更新的地址
+     */
+    String getUrl();
+
+    /**
+     * 开始下载更新
+     *
+     * @param updateEntity     更新信息
+     * @param downloadListener 文件下载监听
+     */
+    void startDownload(@NonNull UpdateEntity updateEntity, @Nullable OnFileDownloadListener downloadListener);
+
+    /**
+     * 后台下载
+     */
+    void backgroundDownload();
+
+    /**
+     * 取消下载
+     */
+    void cancelDownload();
+
+    /**
+     * 资源回收
+     */
+    void recycle();
+}

+ 65 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IUpdateChecker.java

@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.proxy;
+
+import java.util.Map;
+
+import androidx.annotation.NonNull;
+
+/**
+ * 版本更新检查器
+ *
+ * @author xuexiang
+ * @since 2018/6/29 下午8:29
+ */
+public interface IUpdateChecker {
+
+    /**
+     * 版本检查之前
+     */
+    void onBeforeCheck();
+
+    /**
+     * 检查应用的版本信息
+     *
+     * @param isGet       是否是get请求
+     * @param url         版本更新的url地址
+     * @param params      请求参数
+     * @param updateProxy 版本更新代理
+     */
+    void checkVersion(boolean isGet, @NonNull String url, @NonNull Map<String, Object> params, @NonNull IUpdateProxy updateProxy);
+
+    /**
+     * 版本检查之后
+     */
+    void onAfterCheck();
+
+    /**
+     * 处理版本检查的结果
+     *
+     * @param result      检查返回的内容
+     * @param updateProxy 版本更新代理
+     */
+    void processCheckResult(@NonNull String result, @NonNull IUpdateProxy updateProxy);
+
+    /**
+     * 未发现新版本
+     *
+     * @param throwable 未发现的原因
+     */
+    void noNewVersion(Throwable throwable);
+}

+ 50 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IUpdateDownloader.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.proxy;
+
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.service.OnFileDownloadListener;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * 版本更新下载器
+ *
+ * @author xuexiang
+ * @since 2018/6/29 下午8:31
+ */
+public interface IUpdateDownloader {
+
+    /**
+     * 开始下载更新
+     *
+     * @param updateEntity     更新信息
+     * @param downloadListener 文件下载监听
+     */
+    void startDownload(@NonNull UpdateEntity updateEntity, @Nullable OnFileDownloadListener downloadListener);
+
+    /**
+     * 取消下载
+     */
+    void cancelDownload();
+
+    /**
+     * 后台下载更新
+     */
+    void backgroundDownload();
+}

+ 120 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IUpdateHttpService.java

@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.proxy;
+
+import java.io.File;
+import java.util.Map;
+
+import androidx.annotation.NonNull;
+
+/**
+ * 版本更新网络请求服务API
+ *
+ * @author xuexiang
+ * @since 2018/6/29 下午8:44
+ */
+public interface IUpdateHttpService {
+
+    /**
+     * 异步get
+     *
+     * @param url      get请求地址
+     * @param params   get参数
+     * @param callBack 回调
+     */
+    void asyncGet(@NonNull String url, @NonNull Map<String, Object> params, @NonNull Callback callBack);
+
+
+    /**
+     * 异步post
+     *
+     * @param url      post请求地址
+     * @param params   post请求参数
+     * @param callBack 回调
+     */
+    void asyncPost(@NonNull String url, @NonNull Map<String, Object> params, @NonNull Callback callBack);
+
+    /**
+     * 文件下载
+     *
+     * @param url      下载地址
+     * @param path     文件保存路径
+     * @param fileName 文件名称
+     * @param callback 文件下载回调
+     */
+    void download(@NonNull String url, @NonNull String path, @NonNull String fileName, @NonNull DownloadCallback callback);
+
+    /**
+     * 取消文件下载
+     *
+     * @param url      下载地址
+     */
+    void cancelDownload(@NonNull String url);
+
+    /**
+     * 网络请求回调
+     */
+    interface Callback {
+        /**
+         * 结果回调
+         *
+         * @param result 结果
+         */
+        void onSuccess(String result);
+
+        /**
+         * 错误回调
+         *
+         * @param throwable 错误提示
+         */
+        void onError(Throwable throwable);
+    }
+
+    /**
+     * 下载回调
+     */
+    interface DownloadCallback {
+        /**
+         * 下载之前
+         */
+        void onStart();
+
+        /**
+         * 更新进度
+         *
+         * @param progress 进度0.00 - 0.50  - 1.00
+         * @param total    文件总大小 单位字节
+         */
+        void onProgress(float progress, long total);
+
+        /**
+         * 结果回调
+         *
+         * @param file 下载好的文件
+         */
+        void onSuccess(File file);
+
+        /**
+         * 错误回调
+         *
+         * @param throwable 错误提示
+         */
+        void onError(Throwable throwable);
+
+    }
+
+}

+ 56 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IUpdateParser.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.proxy;
+
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.listener.IUpdateParseCallback;
+
+/**
+ * 版本更新解析器[异步解析和同步解析方法只需要实现一个就行了,当isAsyncParser为true时需要实现异步解析方法,否则实现同步解析方法]
+ *
+ * @author xuexiang
+ * @since 2018/6/29 下午8:30
+ */
+public interface IUpdateParser {
+
+    /**
+     * [同步解析方法]
+     * <p>
+     * 将请求的json结果解析为版本更新信息实体
+     *
+     * @param json
+     * @return
+     */
+    UpdateEntity parseJson(String json) throws Exception;
+
+    /**
+     * [异步解析方法]
+     * <p>
+     * 将请求的json结果解析为版本更新信息实体
+     *
+     * @param json
+     * @param callback
+     * @return
+     */
+    void parseJson(String json, final IUpdateParseCallback callback) throws Exception;
+
+    /**
+     * @return 是否是异步解析
+     */
+    boolean isAsyncParser();
+
+}

+ 40 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IUpdatePrompter.java

@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.proxy;
+
+import com.xuexiang.xupdate.entity.PromptEntity;
+import com.xuexiang.xupdate.entity.UpdateEntity;
+
+import androidx.annotation.NonNull;
+
+/**
+ * 版本更新提示器
+ *
+ * @author xuexiang
+ * @since 2018/6/29 下午8:35
+ */
+public interface IUpdatePrompter {
+
+    /**
+     * 显示版本更新提示
+     *
+     * @param updateEntity 更新信息
+     * @param updateProxy  更新代理
+     * @param promptEntity 提示界面参数
+     */
+    void showPrompt(@NonNull UpdateEntity updateEntity, @NonNull IUpdateProxy updateProxy, @NonNull PromptEntity promptEntity);
+}

+ 147 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/IUpdateProxy.java

@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.proxy;
+
+import android.content.Context;
+
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.listener.IUpdateParseCallback;
+import com.xuexiang.xupdate.service.OnFileDownloadListener;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * 版本更新代理
+ *
+ * @author xuexiang
+ * @since 2018/7/1 下午9:45
+ */
+public interface IUpdateProxy {
+
+    /**
+     * 获取上下文
+     *
+     * @return 上下文
+     */
+    @Nullable
+    Context getContext();
+
+    /**
+     * 获取版本更新的地址
+     *
+     * @return 版本更新的地址
+     */
+    String getUrl();
+
+    /**
+     * 获取版本更新网络请求服务API
+     *
+     * @return 网络请求服务API
+     */
+    IUpdateHttpService getIUpdateHttpService();
+
+    /**
+     * 开始版本更新
+     */
+    void update();
+
+    //============ICheckerProxy=================//
+
+    /**
+     * 版本检查之前
+     */
+    void onBeforeCheck();
+
+    /**
+     * 执行网络请求,检查应用的版本信息
+     */
+    void checkVersion();
+
+    /**
+     * 版本检查之后
+     */
+    void onAfterCheck();
+
+    /**
+     * 发现新版本
+     *
+     * @param updateEntity 版本更新信息
+     * @param updateProxy  版本更新代理
+     */
+    void findNewVersion(@NonNull UpdateEntity updateEntity, @NonNull IUpdateProxy updateProxy);
+
+    /**
+     * 未发现新版本
+     *
+     * @param throwable 未发现的原因
+     */
+    void noNewVersion(Throwable throwable);
+
+    //=============IParserProxy================//
+
+    /**
+     * 是否是异步解析者
+     *
+     * @return 是否是异步解析者
+     */
+    boolean isAsyncParser();
+
+    /**
+     * 将请求的json结果解析为版本更新信息实体【同步方法】
+     *
+     * @param json 请求的json数据
+     * @return 版本更新信息实体
+     * @throws Exception 解析出错
+     */
+    UpdateEntity parseJson(@NonNull String json) throws Exception;
+
+    /**
+     * 将请求的json结果解析为版本更新信息实体【异步方法】
+     *
+     * @param json     请求的json数据
+     * @param callback 解析回调
+     * @throws Exception 解析出错
+     */
+    void parseJson(@NonNull String json, final IUpdateParseCallback callback) throws Exception;
+
+    //=============IPrompterProxy================//
+
+    /**
+     * 开始下载更新
+     *
+     * @param updateEntity     更新信息
+     * @param downloadListener 文件下载监听
+     */
+    void startDownload(@NonNull UpdateEntity updateEntity, @Nullable OnFileDownloadListener downloadListener);
+
+    /**
+     * 后台下载
+     */
+    void backgroundDownload();
+
+    /**
+     * 取消下载
+     */
+    void cancelDownload();
+
+    /**
+     * 资源回收
+     */
+    void recycle();
+
+}

+ 23 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/impl/AbstractUpdateParser.java

@@ -0,0 +1,23 @@
+package com.xuexiang.xupdate.proxy.impl;
+
+import com.xuexiang.xupdate.listener.IUpdateParseCallback;
+import com.xuexiang.xupdate.proxy.IUpdateParser;
+
+/**
+ * 默认是使用同步解析器,因此异步解析方法不需要实现
+ *
+ * @author xuexiang
+ * @since 2020-02-15 17:56
+ */
+public abstract class AbstractUpdateParser implements IUpdateParser {
+
+    @Override
+    public void parseJson(String json, IUpdateParseCallback callback) throws Exception {
+        //当isAsyncParser为 true时调用该方法
+    }
+
+    @Override
+    public boolean isAsyncParser() {
+        return false;
+    }
+}

+ 40 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/impl/DefaultFileEncryptor.java

@@ -0,0 +1,40 @@
+package com.xuexiang.xupdate.proxy.impl;
+
+import android.text.TextUtils;
+
+import com.xuexiang.xupdate.proxy.IFileEncryptor;
+import com.xuexiang.xupdate.utils.Md5Utils;
+
+import java.io.File;
+
+/**
+ * 默认的文件加密计算使用的是MD5加密
+ *
+ * @author xuexiang
+ * @since 2019-09-06 14:21
+ */
+public class DefaultFileEncryptor implements IFileEncryptor {
+    /**
+     * 加密文件
+     *
+     * @param file
+     * @return
+     */
+    @Override
+    public String encryptFile(File file) {
+        return Md5Utils.getFileMD5(file);
+    }
+
+    /**
+     * 检验文件是否有效(加密是否一致)
+     *
+     * @param encrypt 加密值, 如果encrypt为空,直接认为是有效的
+     * @param file    需要校验的文件
+     * @return 文件是否有效
+     */
+    @Override
+    public boolean isFileValid(String encrypt, File file) {
+        return TextUtils.isEmpty(encrypt) || encrypt.equalsIgnoreCase(encryptFile(file));
+    }
+
+}

+ 61 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/impl/DefaultPrompterProxyImpl.java

@@ -0,0 +1,61 @@
+package com.xuexiang.xupdate.proxy.impl;
+
+import com.xuexiang.xupdate._XUpdate;
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.proxy.IPrompterProxy;
+import com.xuexiang.xupdate.proxy.IUpdateProxy;
+import com.xuexiang.xupdate.service.OnFileDownloadListener;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * 默认版本更新提示器代理
+ *
+ * @author xuexiang
+ * @since 2020/6/9 12:19 AM
+ */
+public class DefaultPrompterProxyImpl implements IPrompterProxy {
+
+    private IUpdateProxy mUpdateProxy;
+
+    DefaultPrompterProxyImpl(IUpdateProxy proxy) {
+        mUpdateProxy = proxy;
+    }
+
+    @Override
+    public String getUrl() {
+        return mUpdateProxy != null ? mUpdateProxy.getUrl() : "";
+    }
+
+    @Override
+    public void startDownload(@NonNull UpdateEntity updateEntity, @Nullable OnFileDownloadListener downloadListener) {
+        if (mUpdateProxy != null) {
+            mUpdateProxy.startDownload(updateEntity, downloadListener);
+        }
+    }
+
+    @Override
+    public void backgroundDownload() {
+        if (mUpdateProxy != null) {
+            mUpdateProxy.backgroundDownload();
+        }
+    }
+
+    @Override
+    public void cancelDownload() {
+        _XUpdate.setIsPrompterShow(getUrl(), false);
+        if (mUpdateProxy != null) {
+            mUpdateProxy.cancelDownload();
+        }
+    }
+
+    @Override
+    public void recycle() {
+        if (mUpdateProxy != null) {
+            mUpdateProxy.recycle();
+            mUpdateProxy = null;
+        }
+    }
+
+}

+ 155 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/impl/DefaultUpdateChecker.java

@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.proxy.impl;
+
+import android.text.TextUtils;
+
+import com.xuexiang.xupdate._XUpdate;
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.listener.IUpdateParseCallback;
+import com.xuexiang.xupdate.proxy.IUpdateChecker;
+import com.xuexiang.xupdate.proxy.IUpdateHttpService;
+import com.xuexiang.xupdate.proxy.IUpdateProxy;
+import com.xuexiang.xupdate.service.DownloadService;
+import com.xuexiang.xupdate.utils.UpdateUtils;
+
+import java.util.Map;
+
+import androidx.annotation.NonNull;
+
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.CHECK_JSON_EMPTY;
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.CHECK_NET_REQUEST;
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.CHECK_NO_NEW_VERSION;
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.CHECK_PARSE;
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.CHECK_UPDATING;
+
+/**
+ * 默认版本更新检查者
+ *
+ * @author xuexiang
+ * @since 2018/7/2 下午10:21
+ */
+public class DefaultUpdateChecker implements IUpdateChecker {
+
+    @Override
+    public void onBeforeCheck() {
+
+    }
+
+    @Override
+    public void checkVersion(boolean isGet, @NonNull final String url, @NonNull Map<String, Object> params, final @NonNull IUpdateProxy updateProxy) {
+        if (DownloadService.isRunning() || _XUpdate.getCheckUrlStatus(url) || _XUpdate.isPrompterShow(url)) {
+            updateProxy.onAfterCheck();
+            _XUpdate.onUpdateError(CHECK_UPDATING);
+            return;
+        }
+
+        _XUpdate.setCheckUrlStatus(url, true);
+
+        if (isGet) {
+            updateProxy.getIUpdateHttpService().asyncGet(url, params, new IUpdateHttpService.Callback() {
+                @Override
+                public void onSuccess(String result) {
+                    onCheckSuccess(url, result, updateProxy);
+                }
+
+                @Override
+                public void onError(Throwable error) {
+                    onCheckError(url, updateProxy, error);
+                }
+            });
+        } else {
+            updateProxy.getIUpdateHttpService().asyncPost(url, params, new IUpdateHttpService.Callback() {
+                @Override
+                public void onSuccess(String result) {
+                    onCheckSuccess(url, result, updateProxy);
+                }
+
+                @Override
+                public void onError(Throwable error) {
+                    onCheckError(url, updateProxy, error);
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onAfterCheck() {
+
+    }
+
+    /**
+     * 查询成功
+     *
+     * @param url         查询地址
+     * @param result      查询结果
+     * @param updateProxy 更新代理
+     */
+    private void onCheckSuccess(String url, String result, @NonNull IUpdateProxy updateProxy) {
+        _XUpdate.setCheckUrlStatus(url, false);
+        updateProxy.onAfterCheck();
+        if (!TextUtils.isEmpty(result)) {
+            processCheckResult(result, updateProxy);
+        } else {
+            _XUpdate.onUpdateError(CHECK_JSON_EMPTY);
+        }
+    }
+
+    /**
+     * 查询失败
+     *
+     * @param url         查询地址
+     * @param updateProxy 更新代理
+     * @param error       错误
+     */
+    private void onCheckError(String url, @NonNull IUpdateProxy updateProxy, Throwable error) {
+        _XUpdate.setCheckUrlStatus(url, false);
+        updateProxy.onAfterCheck();
+        _XUpdate.onUpdateError(CHECK_NET_REQUEST, error.getMessage());
+    }
+
+    @Override
+    public void processCheckResult(final @NonNull String result, final @NonNull IUpdateProxy updateProxy) {
+        try {
+            if (updateProxy.isAsyncParser()) {
+                //异步解析
+                updateProxy.parseJson(result, new IUpdateParseCallback() {
+                    @Override
+                    public void onParseResult(UpdateEntity updateEntity) {
+                        try {
+                            UpdateUtils.processUpdateEntity(updateEntity, result, updateProxy);
+                        } catch (Exception e) {
+                            e.printStackTrace();
+                            _XUpdate.onUpdateError(CHECK_PARSE, e.getMessage());
+                        }
+                    }
+                });
+            } else {
+                //同步解析
+                UpdateUtils.processUpdateEntity(updateProxy.parseJson(result), result, updateProxy);
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+            _XUpdate.onUpdateError(CHECK_PARSE, e.getMessage());
+        }
+    }
+
+    @Override
+    public void noNewVersion(Throwable throwable) {
+        _XUpdate.onUpdateError(CHECK_NO_NEW_VERSION, throwable != null ? throwable.getMessage() : null);
+    }
+}

+ 176 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/impl/DefaultUpdateDownloader.java

@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.proxy.impl;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.net.Uri;
+import android.os.IBinder;
+import android.text.TextUtils;
+
+import com.xuexiang.xupdate.XUpdate;
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.proxy.IUpdateDownloader;
+import com.xuexiang.xupdate.service.DownloadService;
+import com.xuexiang.xupdate.service.OnFileDownloadListener;
+import com.xuexiang.xupdate.utils.UpdateUtils;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * 默认版本更新下载器
+ *
+ * @author xuexiang
+ * @since 2018/7/5 下午5:06
+ */
+public class DefaultUpdateDownloader implements IUpdateDownloader {
+
+    private DownloadService.DownloadBinder mDownloadBinder;
+
+    /**
+     * 服务绑定连接
+     */
+    private ServiceConnection mServiceConnection;
+
+    /**
+     * 是否已绑定下载服务
+     */
+    private boolean mIsBound;
+
+    @Override
+    public void startDownload(final @NonNull UpdateEntity updateEntity, final @Nullable OnFileDownloadListener downloadListener) {
+        if (isDownloadUrl(updateEntity)) {
+            startDownloadService(updateEntity, downloadListener);
+        } else {
+            startOpenHtml(updateEntity, downloadListener);
+        }
+    }
+
+    /**
+     * 地址是否是下载地址,需要开启下载服务进行下载【可以根据自己的逻辑进行重写】
+     *
+     * @param updateEntity 版本更新信息
+     * @return 地址是否是下载地址
+     */
+    protected boolean isDownloadUrl(@NonNull UpdateEntity updateEntity) {
+        return isApkDownloadUrl(updateEntity) || !isStaticHtmlUrl(updateEntity);
+    }
+
+    /**
+     * 地址是否是apk的下载地址
+     *
+     * @param updateEntity 版本更新信息
+     * @return 地址是否是apk的下载地址
+     */
+    protected boolean isApkDownloadUrl(@NonNull UpdateEntity updateEntity) {
+        String downloadUrl = updateEntity.getDownloadUrl();
+        return !TextUtils.isEmpty(downloadUrl) && downloadUrl.substring(downloadUrl.lastIndexOf("/") + 1).endsWith(".apk");
+    }
+
+    /**
+     * 地址是否是静态网页
+     *
+     * @param updateEntity 版本更新信息
+     * @return 地址是否是静态网页
+     */
+    protected boolean isStaticHtmlUrl(@NonNull UpdateEntity updateEntity) {
+        String downloadUrl = updateEntity.getDownloadUrl();
+        if (TextUtils.isEmpty(downloadUrl)) {
+            return false;
+        }
+        String urlContent = downloadUrl.substring(downloadUrl.lastIndexOf("/") + 1);
+        return urlContent.contains(".htm") || urlContent.contains(".shtm");
+    }
+
+
+    /**
+     * 开启下载服务
+     *
+     * @param updateEntity     版本更新信息
+     * @param downloadListener 下载监听
+     */
+    protected void startDownloadService(@NonNull final UpdateEntity updateEntity, @Nullable final OnFileDownloadListener downloadListener) {
+        DownloadService.bindService(mServiceConnection = new ServiceConnection() {
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder service) {
+                mIsBound = true;
+                startDownload((DownloadService.DownloadBinder) service, updateEntity, downloadListener);
+            }
+
+            @Override
+            public void onServiceDisconnected(ComponentName name) {
+                mIsBound = false;
+            }
+        });
+    }
+
+    /**
+     * 使用系统的api打开网页
+     *
+     * @param updateEntity     版本更新信息
+     * @param downloadListener 监听回调
+     */
+    protected void startOpenHtml(@NonNull UpdateEntity updateEntity, @Nullable OnFileDownloadListener downloadListener) {
+        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(updateEntity.getDownloadUrl()));
+        boolean result = UpdateUtils.startActivity(intent);
+        if (downloadListener != null) {
+            if (result) {
+                // 强制更新的话,不能关闭更新弹窗
+                if (!updateEntity.isForce()) {
+                    downloadListener.onCompleted(null);
+                }
+            } else {
+                downloadListener.onError(null);
+            }
+        }
+    }
+
+    /**
+     * 开始下载
+     *
+     * @param binder           下载服务绑定
+     * @param updateEntity     版本更新信息
+     * @param downloadListener 下载监听
+     */
+    private void startDownload(DownloadService.DownloadBinder binder, @NonNull UpdateEntity updateEntity, @Nullable OnFileDownloadListener downloadListener) {
+        mDownloadBinder = binder;
+        mDownloadBinder.start(updateEntity, downloadListener);
+    }
+
+    @Override
+    public void cancelDownload() {
+        if (mDownloadBinder != null) {
+            mDownloadBinder.stop("取消下载");
+        }
+        if (mIsBound && mServiceConnection != null) {
+            XUpdate.getContext().unbindService(mServiceConnection);
+            mIsBound = false;
+        }
+    }
+
+    /**
+     * 后台下载更新
+     */
+    @Override
+    public void backgroundDownload() {
+        if (mDownloadBinder != null) {
+            mDownloadBinder.showNotification();
+        }
+    }
+}

+ 247 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/impl/DefaultUpdateParser.java

@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.proxy.impl;
+
+import android.text.TextUtils;
+
+import com.xuexiang.xupdate.XUpdate;
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.logs.UpdateLog;
+import com.xuexiang.xupdate.utils.UpdateUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * 默认版本更新解析器【使用JSONObject进行解析,减少第三方的依赖】
+ *
+ * @author xuexiang
+ * @since 2018/7/5 下午4:36
+ */
+public class DefaultUpdateParser extends AbstractUpdateParser {
+
+    @Override
+    public UpdateEntity parseJson(String json) throws Exception {
+        if (!TextUtils.isEmpty(json)) {
+            JSONObject jsonObject = new JSONObject(json);
+            if (jsonObject.has(APIKeyUpper.CODE)) {
+                // 首字母大写的Json
+                return parseDefaultUpperFormatJson(jsonObject);
+            } else {
+                // 首字母小写的Json
+                return parseDefaultLowerFormatJson(jsonObject);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 解析默认接口字段为首字母大写的Json
+     */
+    private UpdateEntity parseDefaultUpperFormatJson(JSONObject jsonObject) throws JSONException {
+        int code = jsonObject.getInt(APIKeyUpper.CODE);
+        if (code == APIConstant.REQUEST_SUCCESS) {
+            int versionCode = jsonObject.getInt(APIKeyUpper.VERSION_CODE);
+            String versionName = jsonObject.optString(APIKeyUpper.VERSION_NAME);
+            int updateStatus = checkUpdateStatus(jsonObject.getInt(APIKeyUpper.UPDATE_STATUS), versionCode, versionName);
+            UpdateEntity updateEntity = new UpdateEntity();
+            if (updateStatus == APIConstant.NO_NEW_VERSION) {
+                updateEntity.setHasUpdate(false);
+            } else {
+                if (updateStatus == APIConstant.HAVE_NEW_VERSION_FORCED_UPDATE) {
+                    updateEntity.setForce(true);
+                } else if (updateStatus == APIConstant.HAVE_NEW_VERSION_IGNORE_UPDATE) {
+                    updateEntity.setIsIgnorable(true);
+                }
+                updateEntity.setHasUpdate(true)
+                        .setUpdateContent(jsonObject.getString(APIKeyUpper.MODIFY_CONTENT))
+                        .setVersionCode(versionCode)
+                        .setVersionName(versionName)
+                        .setDownloadUrl(jsonObject.getString(APIKeyUpper.DOWNLOAD_URL))
+                        .setSize(jsonObject.optLong(APIKeyUpper.APK_SIZE))
+                        .setMd5(jsonObject.optString(APIKeyUpper.APK_MD5));
+            }
+            return updateEntity;
+        }
+        return null;
+    }
+
+
+    /**
+     * 解析默认接口字段为首字母小写的Json
+     */
+    private UpdateEntity parseDefaultLowerFormatJson(JSONObject object) throws JSONException {
+        int code = object.getInt(APIKeyLower.CODE);
+        if (code == APIConstant.REQUEST_SUCCESS) {
+            JSONObject jsonObject = (JSONObject) object.get("data");
+            int versionCode = jsonObject.getInt(APIKeyLower.VERSION_CODE);
+            String versionName = jsonObject.optString(APIKeyLower.VERSION_NAME);
+            int updateStatus = checkUpdateStatus(jsonObject.getInt(APIKeyLower.UPDATE_STATUS), versionCode, versionName);
+            UpdateEntity updateEntity = new UpdateEntity();
+            if (updateStatus == APIConstant.NO_NEW_VERSION) {
+                updateEntity.setHasUpdate(false);
+            } else {
+                if (updateStatus == APIConstant.HAVE_NEW_VERSION_FORCED_UPDATE) {
+                    updateEntity.setForce(true);
+                } else if (updateStatus == APIConstant.HAVE_NEW_VERSION) {
+                    updateEntity.setIsIgnorable(true);
+                }
+                updateEntity.setHasUpdate(true)
+                        .setUpdateContent(jsonObject.getString(APIKeyLower.MODIFY_CONTENT))
+                        .setVersionCode(versionCode)
+                        .setVersionName(versionName)
+                        .setDownloadUrl(jsonObject.getString(APIKeyLower.DOWNLOAD_URL))
+                        .setSize(jsonObject.optLong(APIKeyLower.APK_SIZE))
+                        .setMd5(jsonObject.optString(APIKeyLower.APK_MD5));
+            }
+            return updateEntity;
+        }
+        return null;
+    }
+
+    /**
+     * 本地校验版本更新的状态。【默认处理:当最新版本小于等于应用当前的版本时,不需要更新。】
+     * 【注意:这里只是用于本地校验,应当以云端为主,如果云端没有判断逻辑,才会移至本地】
+     *
+     * 【==可重写该方法进行自定义处理==】
+     *
+     * @param updateStatus     更新状态
+     * @param cloudVersionCode 云端获取的版本号
+     * @param cloudVersionName 云端获取的版本名称
+     * @return 版本更新的状态
+     */
+    protected int checkUpdateStatus(int updateStatus, int cloudVersionCode, String cloudVersionName) {
+        if (updateStatus == APIConstant.NO_NEW_VERSION) {
+            // 优先以云端版本为主
+            return updateStatus;
+        }
+        int localVersionCode = UpdateUtils.getVersionCode(XUpdate.getContext());
+        if (cloudVersionCode <= localVersionCode) {
+            UpdateLog.i("云端获取的最新版本小于等于应用当前的版本,不需要更新!当前版本:" + localVersionCode + ", 云端版本:" + cloudVersionCode);
+            updateStatus = APIConstant.NO_NEW_VERSION;
+        }
+        return updateStatus;
+    }
+
+    /**
+     * 默认接口的API Key【所有接口字段首字母大写】
+     */
+    public interface APIKeyUpper {
+        /**
+         * 请求返回码
+         */
+        String CODE = "Code";
+        /**
+         * 更新的状态
+         */
+        String UPDATE_STATUS = "UpdateStatus";
+        /**
+         * 最新版本号[根据版本号来判别是否需要升级]
+         */
+        String VERSION_CODE = "VersionCode";
+        /**
+         * APP变更的内容
+         */
+        String MODIFY_CONTENT = "ModifyContent";
+        /**
+         * 最新APP版本的名称[用于展示的版本名]
+         */
+        String VERSION_NAME = "VersionName";
+        /**
+         * 下载地址
+         */
+        String DOWNLOAD_URL = "DownloadUrl";
+        /**
+         * Apk大小【单位:KB】
+         */
+        String APK_SIZE = "ApkSize";
+        /**
+         * Apk MD5值
+         */
+        String APK_MD5 = "ApkMd5";
+    }
+
+
+    /**
+     * 默认接口的API Key【所有接口字段首字母小写】
+     */
+    public interface APIKeyLower {
+        /**
+         * 请求返回码
+         */
+        String CODE = "code";
+        /**
+         * 更新的状态
+         */
+        String UPDATE_STATUS = "updateStatus";
+        /**
+         * 最新版本号[根据版本号来判别是否需要升级]
+         */
+        String VERSION_CODE = "versionCode";
+        /**
+         * APP变更的内容
+         */
+        String MODIFY_CONTENT = "modifyContent";
+        /**
+         * 最新APP版本的名称[用于展示的版本名]
+         */
+        String VERSION_NAME = "versionName";
+        /**
+         * 下载地址
+         */
+        String DOWNLOAD_URL = "downloadUrl";
+        /**
+         * Apk大小【单位:KB】
+         */
+        String APK_SIZE = "apkSize";
+        /**
+         * Apk MD5值
+         */
+        String APK_MD5 = "apkMd5";
+    }
+
+    /**
+     * 默认接口的API常量
+     * <p>
+     * 0:无版本更新
+     * 1:有版本更新,不需要强制升级
+     * 2:有版本更新,需要强制升级
+     */
+    public interface APIConstant {
+        /**
+         * 请求成功的code码
+         */
+        int REQUEST_SUCCESS = 100;
+        /**
+         * 无版本更新
+         */
+        int NO_NEW_VERSION = 0;
+        /**
+         * 有版本更新,不需要强制升级
+         */
+        int HAVE_NEW_VERSION = 1;
+        /**
+         * 有版本更新,需要强制升级
+         */
+        int HAVE_NEW_VERSION_FORCED_UPDATE = 2;
+        /**
+         * 有版本更新, 可忽略的版本升级
+         */
+        int HAVE_NEW_VERSION_IGNORE_UPDATE = 3;
+    }
+
+}

+ 91 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/proxy/impl/DefaultUpdatePrompter.java

@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.proxy.impl;
+
+import android.app.Activity;
+import android.content.Context;
+
+import com.xuexiang.xupdate.entity.PromptEntity;
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.logs.UpdateLog;
+import com.xuexiang.xupdate.proxy.IPrompterProxy;
+import com.xuexiang.xupdate.proxy.IUpdatePrompter;
+import com.xuexiang.xupdate.proxy.IUpdateProxy;
+import com.xuexiang.xupdate.widget.UpdateDialog;
+import com.xuexiang.xupdate.widget.UpdateDialogActivity;
+import com.xuexiang.xupdate.widget.UpdateDialogFragment;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.FragmentActivity;
+
+/**
+ * 默认的更新提示器
+ *
+ * @author xuexiang
+ * @since 2018/7/2 下午4:05
+ */
+public class DefaultUpdatePrompter implements IUpdatePrompter {
+
+    /**
+     * 显示版本更新提示
+     *
+     * @param updateEntity 更新信息
+     * @param updateProxy  更新代理
+     * @param promptEntity 提示界面参数
+     */
+    @Override
+    public void showPrompt(@NonNull UpdateEntity updateEntity, @NonNull IUpdateProxy updateProxy, @NonNull PromptEntity promptEntity) {
+        Context context = updateProxy.getContext();
+        if (context == null) {
+            UpdateLog.e("showPrompt failed, context is null!");
+            return;
+        }
+        beforeShowPrompt(updateEntity, promptEntity);
+        UpdateLog.d("[DefaultUpdatePrompter] showPrompt, " + promptEntity);
+        if (context instanceof FragmentActivity) {
+            UpdateDialogFragment.show(((FragmentActivity) context).getSupportFragmentManager(), updateEntity, getPrompterProxy(updateProxy), promptEntity);
+        } else if (context instanceof Activity) {
+            UpdateDialog.newInstance(context, updateEntity, getPrompterProxy(updateProxy), promptEntity).show();
+        } else {
+            UpdateDialogActivity.show(context, updateEntity, getPrompterProxy(updateProxy), promptEntity);
+        }
+    }
+
+    /**
+     * 显示版本更新提示之前的处理【可自定义属于自己的显示逻辑】
+     *
+     * @param updateEntity 更新信息
+     * @param promptEntity 提示界面参数
+     */
+    protected void beforeShowPrompt(@NonNull UpdateEntity updateEntity, @NonNull PromptEntity promptEntity) {
+        // 如果是强制更新的话,默认设置是否忽略下载异常为true,保证即使是下载异常也不退出提示。
+        if (updateEntity.isForce()) {
+            promptEntity.setIgnoreDownloadError(true);
+        }
+    }
+
+    /**
+     * 构建版本更新提示器代理【可自定义属于自己的业务逻辑】
+     *
+     * @param updateProxy 版本更新代理
+     * @return 版本更新提示器代理
+     */
+    protected IPrompterProxy getPrompterProxy(@NonNull IUpdateProxy updateProxy) {
+        return new DefaultPrompterProxyImpl(updateProxy);
+    }
+
+}

+ 508 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/service/DownloadService.java

@@ -0,0 +1,508 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.service;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.text.TextUtils;
+
+import com.xuexiang.xupdate.R;
+import com.xuexiang.xupdate.XUpdate;
+import com.xuexiang.xupdate._XUpdate;
+import com.xuexiang.xupdate.entity.DownloadEntity;
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.logs.UpdateLog;
+import com.xuexiang.xupdate.proxy.IUpdateHttpService;
+import com.xuexiang.xupdate.utils.ApkInstallUtils;
+import com.xuexiang.xupdate.utils.FileUtils;
+import com.xuexiang.xupdate.utils.UpdateUtils;
+
+import java.io.File;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.NotificationCompat;
+
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.DOWNLOAD_FAILED;
+
+/**
+ * APK下载服务
+ *
+ * @author xuexiang
+ * @since 2018/7/5 上午11:15
+ */
+public class DownloadService extends Service {
+
+    private static final int DOWNLOAD_NOTIFY_ID = 1000;
+
+    private static boolean sIsRunning = false;
+
+    private static final String CHANNEL_ID = "xupdate_channel_id";
+    private static final CharSequence CHANNEL_NAME = "xupdate_channel_name";
+
+    private NotificationManager mNotificationManager;
+    private NotificationCompat.Builder mBuilder;
+
+    //=====================绑定服务============================//
+
+    /**
+     * 绑定服务
+     *
+     * @param connection 服务连接
+     */
+    public static void bindService(ServiceConnection connection) {
+        Intent intent = new Intent(XUpdate.getContext(), DownloadService.class);
+        XUpdate.getContext().startService(intent);
+        XUpdate.getContext().bindService(intent, connection, Context.BIND_AUTO_CREATE);
+        sIsRunning = true;
+    }
+
+    /**
+     * 停止下载服务
+     *
+     * @param contentText
+     */
+    private void stop(String contentText) {
+        if (mBuilder != null) {
+            mBuilder.setContentTitle(UpdateUtils.getAppName(DownloadService.this))
+                    .setContentText(contentText);
+            Notification notification = mBuilder.build();
+            notification.flags = Notification.FLAG_AUTO_CANCEL;
+            mNotificationManager.notify(DOWNLOAD_NOTIFY_ID, notification);
+        }
+        close();
+    }
+
+    /**
+     * 关闭服务
+     */
+    private void close() {
+        sIsRunning = false;
+        stopSelf();
+    }
+
+    //=====================生命周期============================//
+
+    /**
+     * 下载服务是否在运行
+     *
+     * @return 是否在运行
+     */
+    public static boolean isRunning() {
+        return sIsRunning;
+    }
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+        mNotificationManager = (NotificationManager) getSystemService(android.content.Context.NOTIFICATION_SERVICE);
+    }
+
+    @Nullable
+    @Override
+    public IBinder onBind(Intent intent) {
+        sIsRunning = true;
+        return new DownloadBinder();
+    }
+
+    @Override
+    public boolean onUnbind(Intent intent) {
+        sIsRunning = false;
+        return super.onUnbind(intent);
+    }
+
+    @Override
+    public void onDestroy() {
+        mNotificationManager = null;
+        mBuilder = null;
+        super.onDestroy();
+    }
+
+    //========================下载通知===================================//
+
+    /**
+     * 创建通知
+     */
+    private void setUpNotification(@NonNull DownloadEntity downloadEntity) {
+        if (!downloadEntity.isShowNotification()) {
+            return;
+        }
+
+        initNotification();
+    }
+
+    /**
+     * 初始化通知
+     */
+    private void initNotification() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
+            //设置绕过免打扰模式
+//            channel.setBypassDnd(false);
+//            //检测是否绕过免打扰模式
+//            channel.canBypassDnd();
+//            //设置在锁屏界面上显示这条通知
+//            channel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
+//            channel.setLightColor(Color.GREEN);
+//            channel.setShowBadge(true);
+//            channel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
+            channel.enableVibration(false);
+            channel.enableLights(false);
+            mNotificationManager.createNotificationChannel(channel);
+        }
+
+        mBuilder = getNotificationBuilder();
+        mNotificationManager.notify(DOWNLOAD_NOTIFY_ID, mBuilder.build());
+    }
+
+    private NotificationCompat.Builder getNotificationBuilder() {
+        return new NotificationCompat.Builder(this, CHANNEL_ID)
+                .setContentTitle(getString(R.string.xupdate_start_download))
+                .setContentText(getString(R.string.xupdate_connecting_service))
+                .setSmallIcon(R.drawable.xupdate_icon_app_update)
+                .setLargeIcon(UpdateUtils.drawable2Bitmap(UpdateUtils.getAppIcon(DownloadService.this)))
+                .setOngoing(true)
+                .setAutoCancel(true)
+                .setWhen(System.currentTimeMillis());
+    }
+
+    /**
+     * DownloadBinder中定义了一些实用的方法
+     *
+     * @author xuexiang
+     * @since 2021/1/24 1:59 AM
+     */
+    public class DownloadBinder extends Binder {
+
+        private FileDownloadCallBack mFileDownloadCallBack;
+
+        private UpdateEntity mUpdateEntity;
+
+        /**
+         * 开始下载
+         *
+         * @param updateEntity     新app信息
+         * @param downloadListener 下载监听
+         */
+        public void start(@NonNull UpdateEntity updateEntity, @Nullable OnFileDownloadListener downloadListener) {
+            //下载
+            mUpdateEntity = updateEntity;
+            startDownload(updateEntity, mFileDownloadCallBack = new FileDownloadCallBack(updateEntity, downloadListener));
+        }
+
+        /**
+         * 停止下载服务
+         *
+         * @param msg
+         */
+        public void stop(String msg) {
+            if (mFileDownloadCallBack != null) {
+                mFileDownloadCallBack.onCancel();
+                mFileDownloadCallBack = null;
+            }
+            if (mUpdateEntity.getIUpdateHttpService() != null) {
+                mUpdateEntity.getIUpdateHttpService().cancelDownload(mUpdateEntity.getDownloadUrl());
+            } else {
+                UpdateLog.e("cancelDownload failed, mUpdateEntity.getIUpdateHttpService() is null!");
+            }
+            DownloadService.this.stop(msg);
+        }
+
+        /**
+         * 显示通知
+         */
+        public void showNotification() {
+            if (mBuilder == null && DownloadService.isRunning()) {
+                initNotification();
+            }
+        }
+    }
+
+    /**
+     * 下载模块
+     */
+    private void startDownload(@NonNull UpdateEntity updateEntity, @NonNull FileDownloadCallBack fileDownloadCallBack) {
+        String apkUrl = updateEntity.getDownloadUrl();
+        if (TextUtils.isEmpty(apkUrl)) {
+            String contentText = getString(R.string.xupdate_tip_download_url_error);
+            stop(contentText);
+            return;
+        }
+        String apkName = UpdateUtils.getApkNameByDownloadUrl(apkUrl);
+
+        File apkCacheDir = FileUtils.getFileByPath(updateEntity.getApkCacheDir());
+        if (apkCacheDir == null) {
+            apkCacheDir = UpdateUtils.getDefaultDiskCacheDir();
+        }
+        try {
+            if (!FileUtils.isFileExists(apkCacheDir)) {
+                apkCacheDir.mkdirs();
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        String target = apkCacheDir + File.separator + updateEntity.getVersionName();
+
+        UpdateLog.d("开始下载更新文件, 下载地址:" + apkUrl + ", 保存路径:" + target + ", 文件名:" + apkName);
+        if (updateEntity.getIUpdateHttpService() != null) {
+            updateEntity.getIUpdateHttpService().download(apkUrl, target, apkName, fileDownloadCallBack);
+        } else {
+            UpdateLog.e("startDownload failed, updateEntity.getIUpdateHttpService() is null!");
+        }
+    }
+
+    /**
+     * 文件下载处理
+     */
+    private class FileDownloadCallBack implements IUpdateHttpService.DownloadCallback {
+
+        private final DownloadEntity mDownloadEntity;
+        /**
+         * 文件下载监听
+         */
+        private OnFileDownloadListener mOnFileDownloadListener;
+
+        /**
+         * 是否下载完成后自动安装
+         */
+        private final boolean mIsAutoInstall;
+
+        private int mOldRate = 0;
+
+        private boolean mIsCancel;
+
+        private final Handler mMainHandler;
+
+        FileDownloadCallBack(@NonNull UpdateEntity updateEntity, @Nullable OnFileDownloadListener listener) {
+            mDownloadEntity = updateEntity.getDownLoadEntity();
+            mIsAutoInstall = updateEntity.isAutoInstall();
+            mOnFileDownloadListener = listener;
+            mMainHandler = new Handler(Looper.getMainLooper());
+        }
+
+        @Override
+        public void onStart() {
+            if (mIsCancel) {
+                return;
+            }
+
+            //清空通知栏状态
+            mNotificationManager.cancel(DOWNLOAD_NOTIFY_ID);
+            mBuilder = null;
+
+            //初始化通知栏
+            setUpNotification(mDownloadEntity);
+            dispatchOnStart();
+        }
+
+        private void dispatchOnStart() {
+            if (UpdateUtils.isMainThread()) {
+                if (mOnFileDownloadListener != null) {
+                    mOnFileDownloadListener.onStart();
+                }
+            } else {
+                mMainHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (mOnFileDownloadListener != null) {
+                            mOnFileDownloadListener.onStart();
+                        }
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void onProgress(float progress, long total) {
+            if (mIsCancel) {
+                return;
+            }
+
+            int rate = Math.round(progress * 100);
+            //做一下判断,防止自回调过于频繁,造成更新通知栏进度过于频繁,而出现卡顿的问题。
+            if (canRefreshProgress(rate)) {
+                dispatchOnProgress(progress, total);
+
+                if (mBuilder != null) {
+                    mBuilder.setContentTitle(getString(R.string.xupdate_lab_downloading) + UpdateUtils.getAppName(DownloadService.this))
+                            .setContentText(rate + "%")
+                            .setProgress(100, rate, false)
+                            .setWhen(System.currentTimeMillis());
+                    Notification notification = mBuilder.build();
+                    notification.flags = Notification.FLAG_AUTO_CANCEL | Notification.FLAG_ONLY_ALERT_ONCE;
+                    mNotificationManager.notify(DOWNLOAD_NOTIFY_ID, notification);
+                }
+                //重新赋值
+                mOldRate = rate;
+            }
+        }
+
+        /**
+         * 是否可以刷新进度
+         *
+         * @param newRate 最新进度
+         * @return 是否可以刷新进度
+         */
+        private boolean canRefreshProgress(int newRate) {
+            if (mBuilder != null) {
+                // 系统通知栏对单个应用通知队列通长度进行了限制。
+                // notify方法会将Notification加入系统的通知队列,当前应用发出的Notification数量超过50时,不再继续向系统的通知队列添加Notification,即造成了notificationManagerCompat.notify(TAG, NOTIFY_ID, notify)无效的现象。
+                return Math.abs(newRate - mOldRate) >= 4;
+            } else {
+                return Math.abs(newRate - mOldRate) >= 1;
+            }
+        }
+
+
+        private void dispatchOnProgress(final float progress, final long total) {
+            if (UpdateUtils.isMainThread()) {
+                if (mOnFileDownloadListener != null) {
+                    mOnFileDownloadListener.onProgress(progress, total);
+                }
+            } else {
+                mMainHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (mOnFileDownloadListener != null) {
+                            mOnFileDownloadListener.onProgress(progress, total);
+                        }
+                    }
+                });
+            }
+        }
+
+        @Override
+        public void onSuccess(final File file) {
+            if (UpdateUtils.isMainThread()) {
+                handleOnSuccess(file);
+            } else {
+                mMainHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        handleOnSuccess(file);
+                    }
+                });
+            }
+        }
+
+        private void handleOnSuccess(File file) {
+            if (mIsCancel) {
+                return;
+            }
+
+            if (mOnFileDownloadListener != null) {
+                if (!mOnFileDownloadListener.onCompleted(file)) {
+                    close();
+                    return;
+                }
+            }
+            UpdateLog.d("更新文件下载完成, 文件路径:" + file.getAbsolutePath());
+            try {
+                if (UpdateUtils.isAppOnForeground(DownloadService.this)) {
+                    //App前台运行
+                    mNotificationManager.cancel(DOWNLOAD_NOTIFY_ID);
+
+                    if (mIsAutoInstall) {
+                        _XUpdate.startInstallApk(DownloadService.this, file, mDownloadEntity);
+                    } else {
+                        showDownloadCompleteNotification(file);
+                    }
+                } else {
+                    showDownloadCompleteNotification(file);
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+            //下载完自杀
+            close();
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            if (mIsCancel) {
+                return;
+            }
+
+            _XUpdate.onUpdateError(DOWNLOAD_FAILED, throwable != null ? throwable.getMessage() : "unknown error!");
+            //App前台运行
+            dispatchOnError(throwable);
+            try {
+                mNotificationManager.cancel(DOWNLOAD_NOTIFY_ID);
+                close();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+
+
+        private void dispatchOnError(final Throwable throwable) {
+            if (UpdateUtils.isMainThread()) {
+                if (mOnFileDownloadListener != null) {
+                    mOnFileDownloadListener.onError(throwable);
+                }
+            } else {
+                mMainHandler.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (mOnFileDownloadListener != null) {
+                            mOnFileDownloadListener.onError(throwable);
+                        }
+                    }
+                });
+            }
+        }
+
+        /**
+         * 取消下载
+         */
+        void onCancel() {
+            mOnFileDownloadListener = null;
+            mIsCancel = true;
+        }
+    }
+
+    private void showDownloadCompleteNotification(File file) {
+        //App后台运行
+        //更新参数,注意flags要使用FLAG_UPDATE_CURRENT
+        Intent installAppIntent = ApkInstallUtils.getInstallAppIntent(file);
+        PendingIntent contentIntent = PendingIntent.getActivity(DownloadService.this, 0, installAppIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        if (mBuilder == null) {
+            mBuilder = getNotificationBuilder();
+        }
+        mBuilder.setContentIntent(contentIntent)
+                .setContentTitle(UpdateUtils.getAppName(DownloadService.this))
+                .setContentText(getString(R.string.xupdate_download_complete))
+                .setProgress(0, 0, false)
+                //                        .setAutoCancel(true)
+                .setDefaults((Notification.DEFAULT_ALL));
+        Notification notification = mBuilder.build();
+        notification.flags = Notification.FLAG_AUTO_CANCEL;
+        mNotificationManager.notify(DOWNLOAD_NOTIFY_ID, notification);
+    }
+}

+ 58 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/service/OnFileDownloadListener.java

@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.service;
+
+import java.io.File;
+
+/**
+ * 下载服务下载监听
+ *
+ * @author xuexiang
+ * @since 2018/7/10 上午10:05
+ */
+public interface OnFileDownloadListener {
+
+    /**
+     * 下载之前
+     */
+    void onStart();
+
+    /**
+     * 更新进度
+     *
+     * @param progress 进度0.00 - 0.50  - 1.00
+     * @param total    文件总大小 单位字节
+     */
+    void onProgress(float progress, long total);
+
+    /**
+     * 下载完毕
+     *
+     * @param file 下载好的文件
+     * @return 下载完毕后是否打开文件进行安装<br>{@code true} :安装<br>{@code false} :不安装
+     */
+    boolean onCompleted(File file);
+
+    /**
+     * 错误回调
+     *
+     * @param throwable 错误提示
+     */
+    void onError(Throwable throwable);
+
+
+}

+ 347 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/ApkInstallUtils.java

@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.xuexiang.xupdate.utils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.os.Build;
+
+import com.xuexiang.xupdate._XUpdate;
+import com.xuexiang.xupdate.utils.ShellUtils.CommandResult;
+
+import java.io.File;
+import java.io.IOException;
+
+import androidx.annotation.RequiresPermission;
+
+import static android.Manifest.permission.INSTALL_PACKAGES;
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.INSTALL_FAILED;
+
+/**
+ * APK安装工具类
+ *
+ * @author xuexiang
+ * @since 2018/7/2 上午1:18
+ */
+public final class ApkInstallUtils {
+    private static final int APP_INSTALL_AUTO = 0;
+    private static final int APP_INSTALL_INTERNAL = 1;
+    private static final int APP_INSTALL_EXTERNAL = 2;
+    /**
+     * apk安装的请求码
+     */
+    public static final int REQUEST_CODE_INSTALL_APP = 999;
+
+    /**
+     * 是否支持静默安装【默认是true】
+     */
+    private static boolean sSupportSilentInstall = true;
+
+    public static boolean isSupportSilentInstall() {
+        return sSupportSilentInstall;
+    }
+
+    /**
+     * 设置是否支持静默安装
+     *
+     * @param supportSilentInstall 是否支持静默安装
+     */
+    public static void setSupportSilentInstall(boolean supportSilentInstall) {
+        ApkInstallUtils.sSupportSilentInstall = supportSilentInstall;
+    }
+
+    private ApkInstallUtils() {
+        throw new UnsupportedOperationException("Do not need instantiate!");
+    }
+
+    /**
+     * 自适应apk安装(如果设备有root权限就自动静默安装)
+     *
+     * @param context
+     * @param apkFile apk文件
+     * @return
+     */
+    public static boolean install(Context context, File apkFile) throws IOException {
+        return isSupportSilentInstall() ? install(context, apkFile.getCanonicalPath()) : installNormal(context, apkFile.getCanonicalPath());
+    }
+
+
+    /**
+     * 自适应apk安装(如果设备有root权限就自动静默安装)
+     *
+     * @param context
+     * @param filePath apk文件的路径
+     * @return
+     */
+    public static boolean install(Context context, String filePath) {
+        if (ApkInstallUtils.isSystemApplication(context)
+                || ShellUtils.checkRootPermission()) {
+            return installAppSilent(context, filePath);
+        }
+        return installNormal(context, filePath);
+    }
+
+    /**
+     * 静默安装 App
+     * <p>非 root 需添加权限
+     * {@code <uses-permission android:name="android.permission.INSTALL_PACKAGES" />}</p>
+     *
+     * @param filePath 文件路径
+     * @return {@code true}: 安装成功<br>{@code false}: 安装失败
+     */
+    @RequiresPermission(INSTALL_PACKAGES)
+    public static boolean installAppSilent(Context context, String filePath) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
+            return installAppSilentBelow24(context, filePath);
+        } else {
+            return installAppSilentAbove24(context.getPackageName(), filePath);
+        }
+    }
+
+    /**
+     * 静默安装 App 在Android7.0以下起作用
+     * <p>非 root 需添加权限
+     * {@code <uses-permission android:name="android.permission.INSTALL_PACKAGES" />}</p>
+     *
+     * @param filePath 文件路径
+     * @return {@code true}: 安装成功<br>{@code false}: 安装失败
+     */
+    @RequiresPermission(INSTALL_PACKAGES)
+    private static boolean installAppSilentBelow24(Context context, String filePath) {
+        File file = FileUtils.getFileByPath(filePath);
+        if (!FileUtils.isFileExists(file)) {
+            return false;
+        }
+
+        String pmParams = " -r " + getInstallLocationParams();
+
+        StringBuilder command = new StringBuilder()
+                .append("LD_LIBRARY_PATH=/vendor/lib:/system/lib pm install ")
+                .append(pmParams).append(" ")
+                .append(filePath.replace(" ", "\\ "));
+        CommandResult commandResult = ShellUtils.execCommand(
+                command.toString(), !isSystemApplication(context), true);
+        return commandResult.successMsg != null
+                && (commandResult.successMsg.contains("Success") || commandResult.successMsg
+                .contains("success"));
+    }
+
+    /**
+     * get params for pm install location
+     *
+     * @return
+     */
+    private static String getInstallLocationParams() {
+        int location = getInstallLocation();
+        switch (location) {
+            case APP_INSTALL_INTERNAL:
+                return "-f";
+            case APP_INSTALL_EXTERNAL:
+                return "-s";
+            default:
+                break;
+        }
+        return "";
+    }
+
+    /**
+     * get system install location<br/>
+     * can be set by System Menu Setting->Storage->Prefered install location
+     *
+     * @return
+     */
+    public static int getInstallLocation() {
+        CommandResult commandResult = ShellUtils
+                .execCommand(
+                        "LD_LIBRARY_PATH=/vendor/lib:/system/lib pm get-install-location",
+                        false, true);
+        if (commandResult.result == 0 && commandResult.successMsg != null
+                && commandResult.successMsg.length() > 0) {
+            try {
+                int location = Integer.parseInt(commandResult.successMsg
+                        .substring(0, 1));
+                switch (location) {
+                    case APP_INSTALL_INTERNAL:
+                        return APP_INSTALL_INTERNAL;
+                    case APP_INSTALL_EXTERNAL:
+                        return APP_INSTALL_EXTERNAL;
+                    default:
+                        break;
+                }
+            } catch (NumberFormatException e) {
+                e.printStackTrace();
+            }
+        }
+        return APP_INSTALL_AUTO;
+    }
+
+    //===============================//
+
+    /**
+     * 静默安装 App 在Android7.0及以上起作用
+     * <p>非 root 需添加权限
+     * {@code <uses-permission android:name="android.permission.INSTALL_PACKAGES" />}</p>
+     *
+     * @param filePath 文件路径
+     * @return {@code true}: 安装成功<br>{@code false}: 安装失败
+     */
+    @RequiresPermission(INSTALL_PACKAGES)
+    private static boolean installAppSilentAbove24(String packageName, String filePath) {
+        File file = FileUtils.getFileByPath(filePath);
+        if (!FileUtils.isFileExists(file)) {
+            return false;
+        }
+        boolean isRoot = isDeviceRooted();
+        String command = "pm install -i " + packageName + " --user 0 " + filePath;
+        CommandResult commandResult = ShellUtils.execCommand(command, isRoot);
+        return (commandResult.successMsg != null
+                && commandResult.successMsg.toLowerCase().contains("success"));
+    }
+
+    /**
+     * 使用系统的意图安装
+     *
+     * @param context
+     * @param filePath file path of package
+     * @return whether apk exist
+     */
+    private static boolean installNormal(Context context, String filePath) {
+        File file = FileUtils.getFileByPath(filePath);
+        return FileUtils.isFileExists(file) && installNormal(context, file);
+    }
+
+    /**
+     * 使用系统的意图进行apk安装
+     *
+     * @param context 上下文
+     * @param appFile 应用文件
+     * @return 安装是否成功
+     */
+    private static boolean installNormal(Context context, File appFile) {
+        try {
+            Intent intent = getInstallAppIntent(appFile);
+            if (context.getPackageManager().queryIntentActivities(intent, 0).size() > 0) {
+                if (context instanceof Activity) {
+                    ((Activity) context).startActivityForResult(intent, REQUEST_CODE_INSTALL_APP);
+                } else {
+                    context.startActivity(intent);
+                }
+                return true;
+            }
+        } catch (Exception e) {
+            _XUpdate.onUpdateError(INSTALL_FAILED, "Apk installation failed using the intent of the system!");
+        }
+        return false;
+    }
+
+    /**
+     * 获取安装apk的意图
+     *
+     * @param appFile
+     * @return
+     */
+    public static Intent getInstallAppIntent(File appFile) {
+        try {
+            Intent intent = new Intent(Intent.ACTION_VIEW);
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                //区别于 FLAG_GRANT_READ_URI_PERMISSION 跟 FLAG_GRANT_WRITE_URI_PERMISSION, URI权限会持久存在即使重启,直到明确的用 revokeUriPermission(Uri, int) 撤销。 这个flag只提供可能持久授权。但是接收的应用必须调用ContentResolver的takePersistableUriPermission(Uri, int)方法实现
+                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
+            }
+            Uri fileUri = FileUtils.getUriByFile(appFile);
+            intent.setDataAndType(fileUri, "application/vnd.android.package-archive");
+            return intent;
+        } catch (Exception e) {
+            _XUpdate.onUpdateError(INSTALL_FAILED, "Failed to get intent for installation!");
+        }
+        return null;
+    }
+
+    /**
+     * 判断设备是否 root
+     *
+     * @return the boolean{@code true}: 是<br>{@code false}: 否
+     */
+    private static boolean isDeviceRooted() {
+        String su = "su";
+        String[] locations = {"/system/bin/", "/system/xbin/", "/sbin/", "/system/sd/xbin/",
+                "/system/bin/failsafe/", "/data/local/xbin/", "/data/local/bin/", "/data/local/"};
+        for (String location : locations) {
+            if (new File(location + su).exists()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+
+    /**
+     * whether context is system application
+     *
+     * @param context
+     * @return
+     */
+    private static boolean isSystemApplication(Context context) {
+        return context != null && isSystemApplication(context, context.getPackageName());
+    }
+
+    /**
+     * whether packageName is system application
+     *
+     * @param context
+     * @param packageName
+     * @return
+     */
+    private static boolean isSystemApplication(Context context,
+                                               String packageName) {
+        return context != null && isSystemApplication(context.getPackageManager(), packageName);
+    }
+
+    /**
+     * whether packageName is system application
+     *
+     * @param packageManager
+     * @param packageName
+     * @return <ul>
+     * <li>if packageManager is null, return false</li>
+     * <li>if package name is null or is empty, return false</li>
+     * <li>if package name not exit, return false</li>
+     * <li>if package name exit, but not system app, return false</li>
+     * <li>else return true</li>
+     * </ul>
+     */
+    private static boolean isSystemApplication(PackageManager packageManager,
+                                               String packageName) {
+        if (packageManager == null || packageName == null
+                || packageName.length() == 0) {
+            return false;
+        }
+        try {
+            ApplicationInfo app = packageManager.getApplicationInfo(
+                    packageName, 0);
+            return (app != null && (app.flags & ApplicationInfo.FLAG_SYSTEM) > 0);
+        } catch (NameNotFoundException e) {
+            e.printStackTrace();
+        }
+        return false;
+    }
+
+}

+ 170 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/ColorUtils.java

@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.utils;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Color;
+
+import java.util.Random;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.ColorRes;
+
+/**
+ * 颜色工具类
+ *
+ * @author xuexiang
+ * @since 2018/7/2 下午3:12
+ */
+public final class ColorUtils {
+
+    private ColorUtils() {
+        throw new UnsupportedOperationException("cannot be instantiated");
+    }
+
+    /**
+     * 颜色选择器
+     *
+     * @param pressedColor 按下的颜色
+     * @param normalColor  正常的颜色
+     * @return 颜色选择器
+     */
+    public static ColorStateList getColorStateList(int pressedColor, int normalColor) {
+        //其他状态默认为白色
+        return new ColorStateList(
+                new int[][]{{android.R.attr.state_enabled, android.R.attr.state_pressed}, {android.R.attr.state_enabled}, {}},
+                new int[]{pressedColor, normalColor, Color.WHITE});
+    }
+
+    /**
+     * 加深颜色
+     *
+     * @param color 原色
+     * @return 加深后的
+     */
+    public static int colorDeep(int color) {
+        int alpha = Color.alpha(color);
+        int red = Color.red(color);
+        int green = Color.green(color);
+        int blue = Color.blue(color);
+        float ratio = 0.8F;
+        red = (int) (red * ratio);
+        green = (int) (green * ratio);
+        blue = (int) (blue * ratio);
+        return Color.argb(alpha, red, green, blue);
+    }
+
+    /**
+     * 是否是深色的颜色
+     *
+     * @param color
+     * @return
+     */
+    public static boolean isColorDark(@ColorInt int color) {
+        double darkness =
+                1
+                        - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color))
+                        / 255;
+        return darkness >= 0.5;
+    }
+
+
+    /**
+     * 按条件的到随机颜色
+     *
+     * @param alpha 透明
+     * @param lower 下边界
+     * @param upper 上边界
+     * @return 颜色值
+     */
+
+    public static int getRandomColor(int alpha, int lower, int upper) {
+        return new RandomColor(alpha, lower, upper).getColor();
+    }
+
+    /**
+     * @return 获取随机色
+     */
+    public static int getRandomColor() {
+        return new RandomColor(255, 80, 200).getColor();
+    }
+
+    /**
+     * 获取Color值
+     *
+     * @param resId
+     * @return
+     */
+    public static int getColor(Context context, @ColorRes int resId) {
+        return context.getResources().getColor(resId);
+    }
+
+    /**
+     * 随机颜色
+     */
+    public static class RandomColor {
+        int alpha;
+        int lower;
+        int upper;
+
+        RandomColor(int alpha, int lower, int upper) {
+            if (upper <= lower) {
+                throw new IllegalArgumentException("must be lower < upper");
+            }
+            setAlpha(alpha);
+            setLower(lower);
+            setUpper(upper);
+        }
+
+        public int getColor() {
+            //随机数是前闭  后开
+            int red = getLower() + new Random().nextInt(getUpper() - getLower() + 1);
+            int green = getLower() + new Random().nextInt(getUpper() - getLower() + 1);
+            int blue = getLower() + new Random().nextInt(getUpper() - getLower() + 1);
+            return Color.argb(getAlpha(), red, green, blue);
+        }
+
+        public int getAlpha() {
+            return alpha;
+        }
+
+        public void setAlpha(int alpha) {
+            if (alpha > 255) alpha = 255;
+            if (alpha < 0) alpha = 0;
+            this.alpha = alpha;
+        }
+
+        int getLower() {
+            return lower;
+        }
+
+        void setLower(int lower) {
+            if (lower < 0) lower = 0;
+            this.lower = lower;
+        }
+
+        int getUpper() {
+            return upper;
+        }
+
+        void setUpper(int upper) {
+            if (upper > 255) upper = 255;
+            this.upper = upper;
+        }
+    }
+}

+ 268 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/DialogUtils.java

@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2021 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.xuexiang.xupdate.utils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.DisplayMetrics;
+import android.view.Display;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.EditText;
+
+/**
+ * 弹窗工具类
+ *
+ * @author xuexiang
+ * @since 2021/11/16 1:19 PM
+ */
+public final class DialogUtils {
+
+    private DialogUtils() {
+        throw new UnsupportedOperationException("u can't instantiate me...");
+    }
+
+    /**
+     * 显示窗口【同步窗口系统view的可见度, 解决全屏下显示窗口导致界面退出全屏的问题】
+     *
+     * @param activity      活动窗口
+     * @param window        需要显示的窗口
+     * @param iWindowShower 窗口显示接口
+     * @return 是否执行成功
+     */
+    public static boolean showWindow(Activity activity, Window window, IWindowShower iWindowShower) {
+        if (activity == null || window == null || iWindowShower == null) {
+            return false;
+        }
+        window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+        iWindowShower.show(window);
+        syncSystemUiVisibility(activity, window);
+        window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+        return true;
+    }
+
+    /**
+     * 同步窗口的系统view的可见度【解决全屏下显示窗口导致界面退出全屏的问题】
+     *
+     * @param original 活动窗口
+     * @param target   目标窗口
+     * @return 是否执行成功
+     */
+    public static boolean syncSystemUiVisibility(Activity original, Window target) {
+        if (original == null) {
+            return false;
+        }
+        return syncSystemUiVisibility(original.getWindow(), target);
+    }
+
+    /**
+     * 同步两个窗口的系统view的可见度【解决全屏下显示窗口导致界面退出全屏的问题】
+     *
+     * @param original 原始窗口
+     * @param target   目标窗口
+     * @return 是否执行成功
+     */
+    public static boolean syncSystemUiVisibility(Window original, Window target) {
+        if (original == null || target == null) {
+            return false;
+        }
+        target.getDecorView().setSystemUiVisibility(original.getDecorView().getSystemUiVisibility());
+        return true;
+    }
+
+    /**
+     * 窗口显示接口
+     */
+    public interface IWindowShower {
+        /**
+         * 显示窗口
+         *
+         * @param window 窗口
+         */
+        void show(Window window);
+    }
+
+    /**
+     * 根据上下文获取Activity
+     *
+     * @param context 上下文
+     * @return Activity
+     */
+    public static Activity findActivity(Context context) {
+        if (context instanceof Activity) {
+            return (Activity) context;
+        }
+        if (context instanceof ContextWrapper) {
+            ContextWrapper wrapper = (ContextWrapper) context;
+            return findActivity(wrapper.getBaseContext());
+        }
+        return null;
+    }
+
+    /**
+     * 根据用户点击的坐标获取用户在窗口上触摸到的View,判断这个View是否是EditText来判断是否需要隐藏键盘
+     *
+     * @param window 窗口
+     * @param event  用户点击事件
+     * @return 是否需要隐藏键盘
+     */
+    public static boolean isShouldHideInput(Window window, MotionEvent event) {
+        if (window == null || event == null) {
+            return false;
+        }
+        if (!isSoftInputShow(window)) {
+            return false;
+        }
+        if (!(window.getCurrentFocus() instanceof EditText)) {
+            return false;
+        }
+        View decorView = window.getDecorView();
+        if (decorView instanceof ViewGroup) {
+            return findTouchEditText((ViewGroup) decorView, event) == null;
+        }
+        return false;
+    }
+
+    private static View findTouchEditText(ViewGroup viewGroup, MotionEvent event) {
+        if (viewGroup == null) {
+            return null;
+        }
+        for (int i = 0; i < viewGroup.getChildCount(); i++) {
+            View child = viewGroup.getChildAt(i);
+            if (child == null || !child.isShown()) {
+                continue;
+            }
+            if (!isTouchView(child, event)) {
+                continue;
+            }
+            if (child instanceof EditText) {
+                return child;
+            } else if (child instanceof ViewGroup) {
+                return findTouchEditText((ViewGroup) child, event);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 判断view是否在触摸区域内
+     *
+     * @param view  view
+     * @param event 点击事件
+     * @return view是否在触摸区域内
+     */
+    private static boolean isTouchView(View view, MotionEvent event) {
+        if (view == null || event == null) {
+            return false;
+        }
+        Rect rect = new Rect();
+        view.getGlobalVisibleRect(rect);
+        return rect.contains((int) event.getX(), (int) event.getY());
+    }
+
+    /**
+     * 输入键盘是否在显示
+     *
+     * @param window 应用窗口
+     */
+    private static boolean isSoftInputShow(Window window) {
+        if (window != null && window.getDecorView() instanceof ViewGroup) {
+            return isSoftInputShow((ViewGroup) window.getDecorView());
+        }
+        return false;
+    }
+
+    /**
+     * 输入键盘是否在显示
+     *
+     * @param rootView 根布局
+     */
+    private static boolean isSoftInputShow(ViewGroup rootView) {
+        if (rootView == null) {
+            return false;
+        }
+        int viewHeight = rootView.getHeight();
+        //获取View可见区域的bottom
+        Rect rect = new Rect();
+        rootView.getWindowVisibleDisplayFrame(rect);
+        int space = viewHeight - rect.bottom - getNavigationBarHeight(rootView.getContext());
+        return space > 0;
+    }
+
+    /**
+     * 获取系统底部导航栏的高度
+     *
+     * @param context 上下文
+     * @return 系统状态栏的高度
+     */
+    private static int getNavigationBarHeight(Context context) {
+        WindowManager windowManager;
+        if (context instanceof Activity) {
+            windowManager = ((Activity) context).getWindowManager();
+        } else {
+            windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+        }
+        if (windowManager == null) {
+            return 0;
+        }
+        Display defaultDisplay = windowManager.getDefaultDisplay();
+        DisplayMetrics realDisplayMetrics = new DisplayMetrics();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            defaultDisplay.getRealMetrics(realDisplayMetrics);
+        }
+        int realHeight = realDisplayMetrics.heightPixels;
+        int realWidth = realDisplayMetrics.widthPixels;
+
+        DisplayMetrics displayMetrics = new DisplayMetrics();
+        defaultDisplay.getMetrics(displayMetrics);
+
+        int displayHeight = displayMetrics.heightPixels;
+        int displayWidth = displayMetrics.widthPixels;
+
+        if (realHeight - displayHeight > 0) {
+            return realHeight - displayHeight;
+        }
+        return Math.max(realWidth - displayWidth, 0);
+    }
+
+    /**
+     * 动态隐藏软键盘
+     *
+     * @param view 视图
+     */
+    public static void hideSoftInput(final View view) {
+        if (view == null) {
+            return;
+        }
+        InputMethodManager imm =
+                (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+        if (imm == null) {
+            return;
+        }
+        imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+    }
+
+
+}

+ 299 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/DrawableUtils.java

@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.utils;
+
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.StateListDrawable;
+import android.os.Build;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * Drawable工具类
+ *
+ * @author xuexiang
+ * @since 2018/7/2 下午3:12
+ */
+public final class DrawableUtils {
+
+    private DrawableUtils() {
+        throw new UnsupportedOperationException("cannot be instantiated");
+    }
+
+    /**
+     * 得到实心的drawable, 一般作为选中,点中的效果
+     *
+     * @param cornerRadius 圆角半径
+     * @param solidColor   实心颜色
+     * @return 得到实心效果
+     */
+    public static GradientDrawable getSolidRectDrawable(int cornerRadius, int solidColor) {
+        GradientDrawable gradientDrawable = new GradientDrawable();
+        // 设置矩形的圆角半径
+        gradientDrawable.setCornerRadius(cornerRadius);
+        // 设置绘画图片色值
+        gradientDrawable.setColor(solidColor);
+        // 绘画的是矩形
+        gradientDrawable.setGradientType(GradientDrawable.RADIAL_GRADIENT);
+        return gradientDrawable;
+    }
+
+    /**
+     * 得到空心的效果,一般作为默认的效果
+     *
+     * @param cornerRadius 圆角半径
+     * @param solidColor   实心颜色
+     * @param strokeColor  边框颜色
+     * @param strokeWidth  边框宽度
+     * @return 得到空心效果
+     */
+    public static GradientDrawable getStrokeRectDrawable(int cornerRadius, int solidColor, int strokeColor, int strokeWidth) {
+        GradientDrawable gradientDrawable = new GradientDrawable();
+        gradientDrawable.setStroke(strokeWidth, strokeColor);
+        gradientDrawable.setColor(solidColor);
+        gradientDrawable.setCornerRadius(cornerRadius);
+        gradientDrawable.setGradientType(GradientDrawable.RADIAL_GRADIENT);
+        return gradientDrawable;
+
+    }
+
+    /**
+     * 背景选择器
+     *
+     * @param pressedDrawable 按下状态的Drawable
+     * @param normalDrawable  正常状态的Drawable
+     * @return 状态选择器
+     */
+    public static StateListDrawable getStateListDrawable(Drawable pressedDrawable, Drawable normalDrawable) {
+        StateListDrawable stateListDrawable = new StateListDrawable();
+        stateListDrawable.addState(new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed}, pressedDrawable);
+        stateListDrawable.addState(new int[]{android.R.attr.state_enabled}, normalDrawable);
+        //设置不能用的状态
+        //默认其他状态背景
+        GradientDrawable gray = getSolidRectDrawable(10, Color.GRAY);
+        stateListDrawable.addState(new int[]{}, gray);
+        return stateListDrawable;
+    }
+
+    /**
+     * 实体  状态选择器
+     *
+     * @param cornerRadius 圆角半径
+     * @param pressedColor 按下颜色
+     * @param normalColor  正常的颜色
+     * @return 状态选择器
+     */
+    public static StateListDrawable getDrawable(int cornerRadius, int pressedColor, int normalColor) {
+        return getStateListDrawable(getSolidRectDrawable(cornerRadius, pressedColor), getSolidRectDrawable(cornerRadius, normalColor));
+    }
+
+    /**
+     * 得到 正常空心, 按下实体的状态选择器
+     *
+     * @param cornerRadiusPX 圆角半径
+     * @param strokeWidthPX  边框宽度
+     * @param subColor       表框颜色
+     * @param mainColor      实心颜色
+     * @return 状态选择器
+     */
+    public static StateListDrawable getStrokeSolidDrawable(int cornerRadiusPX, int strokeWidthPX, int subColor, int mainColor) {
+        //一般solidColor 为透明
+        return getStateListDrawable(
+                //实心
+                getSolidRectDrawable(cornerRadiusPX, subColor),
+                //空心
+                getStrokeRectDrawable(cornerRadiusPX, mainColor, subColor, strokeWidthPX));
+    }
+
+    /**
+     * 得到 正常空心, 按下实体的状态选择器
+     *
+     * @param cornerRadiusPX 圆角半径
+     * @param strokeWidthPX  边框宽度
+     * @param subColor       表框颜色
+     * @param mainColor      实心颜色
+     * @return 状态选择器
+     */
+    public static StateListDrawable getSolidStrokeDrawable(int cornerRadiusPX, int strokeWidthPX, int subColor, int mainColor) {
+        //一般solidColor 为透明
+        return getStateListDrawable(
+                //空心
+                getStrokeRectDrawable(cornerRadiusPX, subColor, mainColor, strokeWidthPX),
+                //实心
+                getSolidRectDrawable(cornerRadiusPX, mainColor));
+    }
+
+    /**
+     * 实体 按下的颜色加深
+     *
+     * @param cornerRadius 圆角半径
+     * @param normalColor  正常的颜色
+     * @return 状态选择器
+     */
+
+    public static StateListDrawable getDrawable(int cornerRadius, int normalColor) {
+        return getDrawable(cornerRadius, ColorUtils.colorDeep(normalColor), normalColor);
+    }
+
+    /**
+     * 实体 得到随机色 状态选择器
+     *
+     * @param cornerRadius 圆角半径
+     * @return 状态选择器
+     */
+
+    public static StateListDrawable getDrawable(int cornerRadius) {
+        return getDrawable(cornerRadius, ColorUtils.getRandomColor());
+    }
+
+    /**
+     * 实体 得到随机色 状态选择器 默认10px
+     *
+     * @return 状态选择器
+     */
+
+    public static StateListDrawable getDrawable() {
+        return getDrawable(10);
+    }
+
+
+    /**
+     * 实心 得到 随机背景色并且带选择器, 并且可以设置圆角
+     *
+     * @param cornerRadius 圆角
+     * @return 状态选择器
+     */
+    public static StateListDrawable getRandomColorDrawable(int cornerRadius) {
+        return getDrawable(cornerRadius, ColorUtils.getRandomColor(), ColorUtils.getRandomColor());
+
+    }
+
+    /**
+     * 实心 得到随机背景色并且带选择器, 并且可以设置圆角
+     *
+     * @return 状态选择器
+     */
+    public static StateListDrawable getRandomColorDrawable() {
+        return getRandomColorDrawable(10);
+
+    }
+
+    /**
+     * 空心,按下实心 得到随机背景色并且带选择器, 并且可以设置圆角
+     *
+     * @return 状态选择器
+     */
+    public static StateListDrawable getStrokeRandomColorDrawable() {
+        return getStrokeSolidDrawable(10, 4, ColorUtils.getRandomColor(), Color.TRANSPARENT);
+    }
+
+    /**
+     * 默认空心 设置TextView 主题,
+     *
+     * @param textView     textView
+     * @param strokeWidth  边框宽度 px
+     * @param cornerRadius 圆角
+     * @param color        颜色
+     */
+    public static void setTextStrokeTheme(TextView textView, int strokeWidth, int cornerRadius, int color) {
+        textView.setBackgroundDrawable(getStrokeSolidDrawable(cornerRadius, strokeWidth, color, Color.WHITE));
+        textView.setTextColor(ColorUtils.getColorStateList(Color.WHITE, color));
+        textView.getPaint().setFlags(Paint.FAKE_BOLD_TEXT_FLAG);
+    }
+
+    /**
+     * 默认空心 设置TextView 主题,随机颜色
+     *
+     * @param textView     textView
+     * @param strokeWidth  边框宽度 px
+     * @param cornerRadius 圆角
+     */
+    public static void setTextStrokeTheme(TextView textView, int strokeWidth, int cornerRadius) {
+        setTextStrokeTheme(textView, strokeWidth, cornerRadius, ColorUtils.getRandomColor());
+    }
+
+    /**
+     * 默认空心 设置TextView 主题,随机颜色
+     *
+     * @param textView textView
+     */
+    public static void setTextStrokeTheme(TextView textView) {
+        setTextStrokeTheme(textView, 6, 10);
+    }
+
+    /**
+     * 默认空心 设置TextView 主题,随机颜色
+     *
+     * @param textView 文本控件
+     * @param color    颜色
+     */
+    public static void setTextStrokeTheme(TextView textView, int color) {
+        setTextStrokeTheme(textView, 6, 10, color);
+    }
+
+    /**
+     * 默认实心 设置TextView 主题,
+     *
+     * @param textView     textView
+     * @param strokeWidth  边框宽度 px
+     * @param cornerRadius 圆角
+     * @param color        颜色
+     */
+    public static void setTextSolidTheme(TextView textView, int strokeWidth, int cornerRadius, int color) {
+        textView.setBackgroundDrawable(getSolidStrokeDrawable(cornerRadius, strokeWidth, Color.WHITE, color));
+        textView.setTextColor(ColorUtils.getColorStateList(color, Color.WHITE));
+        textView.getPaint().setFlags(Paint.FAKE_BOLD_TEXT_FLAG);
+    }
+
+    /**
+     * 默认实心 设置TextView 主题,随机颜色
+     *
+     * @param textView     textView
+     * @param strokeWidth  边框宽度 px
+     * @param cornerRadius 圆角
+     */
+    public static void setTextSolidTheme(TextView textView, int strokeWidth, int cornerRadius) {
+        setTextSolidTheme(textView, strokeWidth, cornerRadius, ColorUtils.getRandomColor());
+    }
+
+    /**
+     * 默认实心 设置TextView 主题,随机颜色
+     *
+     * @param textView textView
+     */
+    public static void setTextSolidTheme(TextView textView) {
+        setTextSolidTheme(textView, 6, 10);
+    }
+
+    /**
+     * 设置控件背景
+     *
+     * @param view 控件
+     * @param d    背景资源
+     */
+    public static void setBackgroundCompat(View view, Drawable d) {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
+            view.setBackgroundDrawable(d);
+        } else {
+            view.setBackground(d);
+        }
+    }
+
+}

+ 428 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/FileUtils.java

@@ -0,0 +1,428 @@
+package com.xuexiang.xupdate.utils;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+
+import com.xuexiang.xupdate.XUpdate;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.core.content.FileProvider;
+
+/**
+ * 文件操作工具类
+ *
+ * @author xuexiang
+ * @since 2020/6/6 11:52 AM
+ */
+public final class FileUtils {
+
+    /**
+     * 只读模式
+     */
+    public static final String MODE_READ_ONLY = "r";
+
+    private static final String EXT_STORAGE_PATH = getExtStoragePath();
+
+    private static final String EXT_STORAGE_DIR = EXT_STORAGE_PATH + File.separator;
+
+    private static final String APP_EXT_STORAGE_PATH = EXT_STORAGE_DIR + "Android";
+
+    private static final String EXT_DOWNLOADS_PATH = getExtDownloadsPath();
+
+    private static final String EXT_PICTURES_PATH = getExtPicturesPath();
+
+    private static final String EXT_DCIM_PATH = getExtDCIMPath();
+
+    private FileUtils() {
+        throw new UnsupportedOperationException("u can't instantiate me...");
+    }
+
+    /**
+     * 根据文件路径获取文件
+     *
+     * @param filePath 文件路径
+     * @return 文件
+     */
+    @Nullable
+    public static File getFileByPath(final String filePath) {
+        return isSpace(filePath) ? null : new File(filePath);
+    }
+
+    /**
+     * 判断文件是否存在
+     *
+     * @param file 文件
+     * @return {@code true}: 存在<br>{@code false}: 不存在
+     */
+    public static boolean isFileExists(final File file) {
+        if (file == null) {
+            return false;
+        }
+        if (file.exists()) {
+            return true;
+        }
+        return isFileExists(file.getAbsolutePath());
+    }
+
+    /**
+     * 判断文件是否存在
+     *
+     * @param filePath 文件路径
+     * @return {@code true}: 存在<br>{@code false}: 不存在
+     */
+    public static boolean isFileExists(final String filePath) {
+        File file = getFileByPath(filePath);
+        if (file == null) {
+            return false;
+        }
+        if (file.exists()) {
+            return true;
+        }
+        return isFileExistsApi29(filePath);
+    }
+
+    /**
+     * Android 10判断文件是否存在的方法
+     *
+     * @param filePath 文件路径
+     * @return {@code true}: 存在<br>{@code false}: 不存在
+     */
+    private static boolean isFileExistsApi29(String filePath) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            AssetFileDescriptor afd = null;
+            try {
+                Uri uri = Uri.parse(filePath);
+                afd = openAssetFileDescriptor(uri);
+                if (afd == null) {
+                    return false;
+                } else {
+                    closeIOQuietly(afd);
+                }
+            } catch (FileNotFoundException e) {
+                return false;
+            } finally {
+                closeIOQuietly(afd);
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * 获取文件输入流
+     *
+     * @param file 文件
+     * @return
+     * @throws FileNotFoundException
+     */
+    public static InputStream getFileInputStream(File file) throws FileNotFoundException {
+        if (isScopedStorageMode()) {
+            return getContentResolver().openInputStream(getUriByFile(file));
+        } else {
+            return new FileInputStream(file);
+        }
+    }
+
+
+    /**
+     * 根据文件获取uri
+     *
+     * @param file 文件
+     * @return
+     */
+    public static Uri getUriByFile(final File file) {
+        if (file == null) {
+            return null;
+        }
+        if (isScopedStorageMode() && isPublicPath(file)) {
+            String filePath = file.getAbsolutePath();
+            if (filePath.startsWith(EXT_DOWNLOADS_PATH)) {
+                return getDownloadContentUri(XUpdate.getContext(), file);
+            } else if (filePath.startsWith(EXT_PICTURES_PATH) || filePath.startsWith(EXT_DCIM_PATH)) {
+                return getMediaContentUri(XUpdate.getContext(), file);
+            } else {
+                return getUriForFile(file);
+            }
+        } else {
+            return getUriForFile(file);
+        }
+    }
+
+
+    /**
+     * Return a content URI for a given file.
+     *
+     * @param file The file.
+     * @return a content URI for a given file
+     */
+    @Nullable
+    public static Uri getUriForFile(final File file) {
+        if (file == null) {
+            return null;
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            String authority = XUpdate.getContext().getPackageName() + ".updateFileProvider";
+            return FileProvider.getUriForFile(XUpdate.getContext(), authority, file);
+        } else {
+            return Uri.fromFile(file);
+        }
+    }
+
+    /**
+     * 是否是分区存储模式:在公共目录下file的api无效了
+     *
+     * @return 是否是分区存储模式
+     */
+    public static boolean isScopedStorageMode() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !Environment.isExternalStorageLegacy();
+    }
+
+    /**
+     * 将媒体文件转化为资源定位符
+     *
+     * @param context
+     * @param mediaFile 媒体文件
+     * @return
+     */
+    public static Uri getMediaContentUri(Context context, File mediaFile) {
+        String filePath = mediaFile.getAbsolutePath();
+        Uri baseUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+        Cursor cursor = context.getContentResolver().query(baseUri,
+                new String[]{MediaStore.Images.Media._ID}, MediaStore.Images.Media.DATA + "=? ",
+                new String[]{filePath}, null);
+        if (cursor != null && cursor.moveToFirst()) {
+            int id = cursor.getInt(cursor.getColumnIndex(MediaStore.MediaColumns._ID));
+            cursor.close();
+            return Uri.withAppendedPath(baseUri, "" + id);
+        } else {
+            if (mediaFile.exists()) {
+                ContentValues values = new ContentValues();
+                values.put(MediaStore.Images.Media.DATA, filePath);
+                return context.getContentResolver().insert(baseUri, values);
+            }
+            return null;
+        }
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.Q)
+    public static Uri getDownloadContentUri(Context context, File file) {
+        String filePath = file.getAbsolutePath();
+        Uri baseUri = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
+        Cursor cursor = context.getContentResolver().query(baseUri,
+                new String[]{MediaStore.Downloads._ID}, MediaStore.Downloads.DATA + "=? ",
+                new String[]{filePath}, null);
+        if (cursor != null && cursor.moveToFirst()) {
+            int id = cursor.getInt(cursor.getColumnIndex(MediaStore.DownloadColumns._ID));
+            cursor.close();
+            return Uri.withAppendedPath(baseUri, "" + id);
+        } else {
+            if (file.exists()) {
+                ContentValues values = new ContentValues();
+                values.put(MediaStore.Downloads.DATA, filePath);
+                return context.getContentResolver().insert(baseUri, values);
+            }
+            return null;
+        }
+    }
+
+    /**
+     * 是否是私有目录
+     *
+     *  <pre>path: /data/data/package/</pre>
+     *  <pre>path: /storage/emulated/0/Android/data/package/</pre>
+     *
+     * @param path 需要判断的目录
+     * @return 是否是私有目录
+     */
+    public static boolean isPrivatePath(@NonNull Context context, @NonNull String path) {
+        if (isSpace(path)) {
+            return false;
+        }
+        String appIntPath = getAppIntPath(context);
+        String appExtPath = getAppExtPath(context);
+        return (!TextUtils.isEmpty(appIntPath) && path.startsWith(appIntPath))
+                || (!TextUtils.isEmpty(appExtPath) && path.startsWith(appExtPath));
+    }
+
+    /**
+     * 是否是公有目录
+     *
+     * @return 是否是公有目录
+     */
+    public static boolean isPublicPath(File file) {
+        if (file == null) {
+            return false;
+        }
+        try {
+            return isPublicPath(file.getCanonicalPath());
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return false;
+    }
+
+    /**
+     * 是否是公有目录
+     *
+     * @return 是否是公有目录
+     */
+    public static boolean isPublicPath(String filePath) {
+        if (isSpace(filePath)) {
+            return false;
+        }
+        return filePath.startsWith(EXT_STORAGE_PATH) && !filePath.startsWith(APP_EXT_STORAGE_PATH);
+    }
+
+    private static boolean isSpace(final String s) {
+        if (s == null) {
+            return true;
+        }
+        for (int i = 0, len = s.length(); i < len; ++i) {
+            if (!Character.isWhitespace(s.charAt(i))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 安静关闭 IO
+     *
+     * @param closeables closeables
+     */
+    public static void closeIOQuietly(final Closeable... closeables) {
+        if (closeables == null) {
+            return;
+        }
+        for (Closeable closeable : closeables) {
+            if (closeable != null) {
+                try {
+                    closeable.close();
+                } catch (IOException ignored) {
+                }
+            }
+        }
+    }
+
+    /**
+     * 从uri资源符中读取文件描述
+     *
+     * @param uri 文本资源符
+     * @return AssetFileDescriptor
+     */
+    public static AssetFileDescriptor openAssetFileDescriptor(Uri uri) throws FileNotFoundException {
+        return getContentResolver().openAssetFileDescriptor(uri, MODE_READ_ONLY);
+    }
+
+    private static ContentResolver getContentResolver() {
+        return XUpdate.getContext().getContentResolver();
+    }
+
+    /**
+     * 获取 Android 外置储存的根目录
+     * <pre>path: /storage/emulated/0</pre>
+     *
+     * @return 外置储存根目录
+     */
+    public static String getExtStoragePath() {
+        return Environment.getExternalStorageDirectory().getAbsolutePath();
+    }
+
+    /**
+     * 获取下载目录
+     * <pre>path: /storage/emulated/0/Download</pre>
+     *
+     * @return 下载目录
+     */
+    public static String getExtDownloadsPath() {
+        return Environment
+                .getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
+                .getAbsolutePath();
+    }
+
+    /**
+     * 获取图片目录
+     * <pre>path: /storage/emulated/0/Pictures</pre>
+     *
+     * @return 图片目录
+     */
+    public static String getExtPicturesPath() {
+        return Environment
+                .getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+                .getAbsolutePath();
+    }
+
+    /**
+     * 获取相机拍摄的照片和视频的目录
+     * <pre>path: /storage/emulated/0/DCIM</pre>
+     *
+     * @return 照片和视频目录
+     */
+    public static String getExtDCIMPath() {
+        return Environment
+                .getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
+                .getAbsolutePath();
+    }
+
+    /**
+     * 获取此应用的私有存储目录
+     * <pre>path: /data/data/package/</pre>
+     *
+     * @return 此应用的缓存目录
+     */
+    public static String getAppIntPath(@NonNull Context context) {
+        File appIntCacheFile = context.getCacheDir();
+        if (appIntCacheFile != null) {
+            String appIntCachePath = appIntCacheFile.getAbsolutePath();
+            return getDirName(appIntCachePath);
+        }
+        return null;
+    }
+
+    /**
+     * 获取此应用在外置储存中的私有存储目录
+     * <pre>path: /storage/emulated/0/Android/data/package/</pre>
+     *
+     * @return 此应用在外置储存中的缓存目录
+     */
+    public static String getAppExtPath(@NonNull Context context) {
+        File appExtCacheFile = context.getExternalCacheDir();
+        if (appExtCacheFile != null) {
+            String appExtCachePath = appExtCacheFile.getAbsolutePath();
+            return getDirName(appExtCachePath);
+        }
+        return null;
+    }
+
+    /**
+     * 获取全路径中的最长目录
+     *
+     * @param filePath 文件路径
+     * @return filePath 最长目录
+     */
+    public static String getDirName(final String filePath) {
+        if (isSpace(filePath)) {
+            return filePath;
+        }
+        int lastSep = filePath.lastIndexOf(File.separator);
+        return lastSep == -1 ? "" : filePath.substring(0, lastSep + 1);
+    }
+
+}

+ 79 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/Md5Utils.java

@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.utils;
+
+import java.io.File;
+import java.io.InputStream;
+import java.security.MessageDigest;
+
+/**
+ * MD5加密工具类
+ *
+ * @author xuexiang
+ * @since 2018/7/2 下午3:14
+ */
+public final class Md5Utils {
+
+    private Md5Utils() {
+        throw new UnsupportedOperationException("cannot be instantiated");
+    }
+
+    /**
+     * 获取文件的MD5值
+     *
+     * @param file
+     * @return
+     */
+    public static String getFileMD5(File file) {
+        if (!FileUtils.isFileExists(file)) {
+            return "";
+        }
+        InputStream fis = null;
+        try {
+            MessageDigest digest = MessageDigest.getInstance("MD5");
+            fis = FileUtils.getFileInputStream(file);
+            byte[] buffer = new byte[8192];
+            int len;
+            while ((len = fis.read(buffer)) != -1) {
+                digest.update(buffer, 0, len);
+            }
+            return bytes2Hex(digest.digest());
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            FileUtils.closeIOQuietly(fis);
+        }
+        return "";
+    }
+
+    /**
+     * 一个byte转为2个hex字符
+     *
+     * @param src byte数组
+     * @return 16进制大写字符串
+     */
+    private static String bytes2Hex(byte[] src) {
+        char[] res = new char[src.length << 1];
+        final char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
+        for (int i = 0, j = 0; i < src.length; i++) {
+            res[j++] = hexDigits[src[i] >>> 4 & 0x0F];
+            res[j++] = hexDigits[src[i] & 0x0F];
+        }
+        return new String(res);
+    }
+
+}

+ 242 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/ShellUtils.java

@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.utils;
+
+import java.io.BufferedReader;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.List;
+
+/**
+ * Shell命令执行工具类
+ * <ul>
+ * <strong>Check root</strong>
+ * <li>{@link ShellUtils#checkRootPermission()}</li>
+ * </ul>
+ * <ul>
+ * <strong>Execute command</strong>
+ * <li>{@link ShellUtils#execCommand(String, boolean)}</li>
+ * <li>{@link ShellUtils#execCommand(String, boolean, boolean)}</li>
+ * <li>{@link ShellUtils#execCommand(List, boolean)}</li>
+ * <li>{@link ShellUtils#execCommand(List, boolean, boolean)}</li>
+ * <li>{@link ShellUtils#execCommand(String[], boolean)}</li>
+ * <li>{@link ShellUtils#execCommand(String[], boolean, boolean)}</li>
+ * </ul>
+ */
+class ShellUtils {
+
+    public static final String COMMAND_SU = "su";
+    public static final String COMMAND_SH = "sh";
+    public static final String COMMAND_EXIT = "exit\n";
+    public static final String COMMAND_LINE_END = "\n";
+
+    private ShellUtils() {
+        throw new UnsupportedOperationException("u can't instantiate me...");
+    }
+
+    /**
+     * check whether has root permission
+     *
+     * @return
+     */
+    public static boolean checkRootPermission() {
+        return execCommand("echo root", true, false).result == 0;
+    }
+
+    /**
+     * execute shell command, default return result msg
+     *
+     * @param command command
+     * @param isRoot  whether need to run with root
+     * @return
+     * @see ShellUtils#execCommand(String[], boolean, boolean)
+     */
+    public static CommandResult execCommand(String command, boolean isRoot) {
+        return execCommand(new String[]{command}, isRoot, true);
+    }
+
+    /**
+     * execute shell commands, default return result msg
+     *
+     * @param commands command list
+     * @param isRoot   whether need to run with root
+     * @return
+     * @see ShellUtils#execCommand(String[], boolean, boolean)
+     */
+    public static CommandResult execCommand(List<String> commands, boolean isRoot) {
+        return execCommand(commands == null ? null : commands.toArray(new String[]{}), isRoot, true);
+    }
+
+    /**
+     * execute shell commands, default return result msg
+     *
+     * @param commands command array
+     * @param isRoot   whether need to run with root
+     * @return
+     * @see ShellUtils#execCommand(String[], boolean, boolean)
+     */
+    public static CommandResult execCommand(String[] commands, boolean isRoot) {
+        return execCommand(commands, isRoot, true);
+    }
+
+    /**
+     * execute shell command
+     *
+     * @param command         command
+     * @param isRoot          whether need to run with root
+     * @param isNeedResultMsg whether need result msg
+     * @return
+     * @see ShellUtils#execCommand(String[], boolean, boolean)
+     */
+    public static CommandResult execCommand(String command, boolean isRoot, boolean isNeedResultMsg) {
+        return execCommand(new String[]{command}, isRoot, isNeedResultMsg);
+    }
+
+    /**
+     * execute shell commands
+     *
+     * @param commands        command list
+     * @param isRoot          whether need to run with root
+     * @param isNeedResultMsg whether need result msg
+     * @return
+     * @see ShellUtils#execCommand(String[], boolean, boolean)
+     */
+    public static CommandResult execCommand(List<String> commands, boolean isRoot, boolean isNeedResultMsg) {
+        return execCommand(commands == null ? null : commands.toArray(new String[]{}), isRoot, isNeedResultMsg);
+    }
+
+    /**
+     * execute shell commands
+     *
+     * @param commands        command array
+     * @param isRoot          whether need to run with root
+     * @param isNeedResultMsg whether need result msg
+     * @return <ul>
+     * <li>if isNeedResultMsg is false, {@link CommandResult#successMsg} is null and
+     * {@link CommandResult#errorMsg} is null.</li>
+     * <li>if {@link CommandResult#result} is -1, there maybe some excepiton.</li>
+     * </ul>
+     */
+    public static CommandResult execCommand(String[] commands, boolean isRoot, boolean isNeedResultMsg) {
+        int result = -1;
+        if (commands == null || commands.length == 0) {
+            return new CommandResult(result, null, null);
+        }
+
+        Process process = null;
+        BufferedReader successResult = null;
+        BufferedReader errorResult = null;
+        StringBuilder successMsg = null;
+        StringBuilder errorMsg = null;
+
+        DataOutputStream os = null;
+        try {
+            process = Runtime.getRuntime().exec(isRoot ? COMMAND_SU : COMMAND_SH);
+            os = new DataOutputStream(process.getOutputStream());
+            for (String command : commands) {
+                if (command == null) {
+                    continue;
+                }
+
+                // donnot use os.writeBytes(commmand), avoid chinese charset error
+                os.write(command.getBytes());
+                os.writeBytes(COMMAND_LINE_END);
+                os.flush();
+            }
+            os.writeBytes(COMMAND_EXIT);
+            os.flush();
+
+            result = process.waitFor();
+            // get command result
+            if (isNeedResultMsg) {
+                successMsg = new StringBuilder();
+                errorMsg = new StringBuilder();
+                successResult = new BufferedReader(new InputStreamReader(process.getInputStream()));
+                errorResult = new BufferedReader(new InputStreamReader(process.getErrorStream()));
+                String s;
+                while ((s = successResult.readLine()) != null) {
+                    successMsg.append(s);
+                }
+                while ((s = errorResult.readLine()) != null) {
+                    errorMsg.append(s);
+                }
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                if (os != null) {
+                    os.close();
+                }
+                if (successResult != null) {
+                    successResult.close();
+                }
+                if (errorResult != null) {
+                    errorResult.close();
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+
+            if (process != null) {
+                process.destroy();
+            }
+        }
+        return new CommandResult(result, successMsg == null ? null : successMsg.toString(), errorMsg == null ? null
+                : errorMsg.toString());
+    }
+
+    /**
+     * result of command
+     * <ul>
+     * <li>{@link CommandResult#result} means result of command, 0 means normal, else means error, same to excute in
+     * linux shell</li>
+     * <li>{@link CommandResult#successMsg} means success message of command result</li>
+     * <li>{@link CommandResult#errorMsg} means error message of command result</li>
+     * </ul>
+     *
+     * @author <a href="http://www.trinea.cn" target="_blank">Trinea</a> 2013-5-16
+     */
+    public static class CommandResult {
+
+        /**
+         * result of command
+         **/
+        public int result;
+        /**
+         * success message of command result
+         **/
+        public String successMsg;
+        /**
+         * error message of command result
+         **/
+        public String errorMsg;
+
+        public CommandResult(int result) {
+            this.result = result;
+        }
+
+        public CommandResult(int result, String successMsg, String errorMsg) {
+            this.result = result;
+            this.successMsg = successMsg;
+            this.errorMsg = errorMsg;
+        }
+    }
+}

+ 23 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/UpdateFileProvider.java

@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.utils;
+
+import androidx.core.content.FileProvider;
+
+public class UpdateFileProvider extends FileProvider {
+
+}

+ 470 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/utils/UpdateUtils.java

@@ -0,0 +1,470 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.utils;
+
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Environment;
+import android.os.Looper;
+import android.text.TextUtils;
+import android.util.DisplayMetrics;
+
+import com.xuexiang.xupdate.R;
+import com.xuexiang.xupdate.XUpdate;
+import com.xuexiang.xupdate._XUpdate;
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.logs.UpdateLog;
+import com.xuexiang.xupdate.proxy.IUpdateProxy;
+
+import java.io.File;
+import java.util.List;
+
+import androidx.annotation.NonNull;
+
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.CHECK_APK_CACHE_DIR_EMPTY;
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.CHECK_IGNORED_VERSION;
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.CHECK_PARSE;
+
+/**
+ * 更新工具类
+ *
+ * @author xuexiang
+ * @since 2018/7/2 下午3:24
+ */
+public final class UpdateUtils {
+
+    private static final String IGNORE_VERSION = "xupdate_ignore_version";
+    private static final String PREFS_FILE = "xupdate_prefs";
+
+    private static final String KEY_XUPDATE = "xupdate";
+
+    private UpdateUtils() {
+        throw new UnsupportedOperationException("cannot be instantiated");
+    }
+
+    //=======================检查========================//
+
+    /**
+     * 处理解析获取到的最新版本更新信息【版本处理的核心】
+     *
+     * @param updateEntity 版本更新信息
+     * @param result       版本的json信息
+     * @param updateProxy  更新代理
+     */
+    public static void processUpdateEntity(UpdateEntity updateEntity, @NonNull String result, @NonNull IUpdateProxy updateProxy) throws Exception {
+        if (updateEntity != null) {
+            if (updateEntity.isHasUpdate()) {
+                //校验是否是已忽略版本
+                if (updateEntity.isIgnorable() && UpdateUtils.isIgnoreVersion(updateProxy.getContext(), updateEntity.getVersionName())) {
+                    _XUpdate.onUpdateError(CHECK_IGNORED_VERSION);
+                    //校验apk下载缓存目录是否为空
+                } else if (TextUtils.isEmpty(updateEntity.getApkCacheDir())) {
+                    _XUpdate.onUpdateError(CHECK_APK_CACHE_DIR_EMPTY);
+                } else {
+                    updateProxy.findNewVersion(updateEntity, updateProxy);
+                }
+            } else {
+                updateProxy.noNewVersion(null);
+            }
+        } else {
+            _XUpdate.onUpdateError(CHECK_PARSE, "json:" + result);
+        }
+    }
+
+    /**
+     * 不能为null
+     *
+     * @param object
+     * @param message
+     * @param <T>
+     * @return
+     */
+    public static <T> T requireNonNull(final T object, final String message) {
+        if (object == null) {
+            throw new NullPointerException(message);
+        }
+        return object;
+    }
+
+    /**
+     * 检测当前网络是否是wifi
+     *
+     * @return 当前网络是否是wifi
+     */
+    public static boolean checkWifi() {
+        ConnectivityManager connectivity = (ConnectivityManager) XUpdate.getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+        if (connectivity == null) {
+            return false;
+        }
+        NetworkInfo info = connectivity.getActiveNetworkInfo();
+        return info != null && info.isConnected() && info.getType() == ConnectivityManager.TYPE_WIFI;
+    }
+
+    /**
+     * 检查当前是否有网
+     *
+     * @return 当前是否有网
+     */
+    public static boolean checkNetwork() {
+        ConnectivityManager connectivity = (ConnectivityManager) XUpdate.getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+        if (connectivity == null) {
+            return false;
+        }
+        NetworkInfo info = connectivity.getActiveNetworkInfo();
+        return info != null && info.isConnected();
+    }
+
+    /**
+     * 获取应用的VersionCode
+     *
+     * @param context
+     * @return
+     */
+    public static int getVersionCode(Context context) {
+        PackageInfo packageInfo = getPackageInfo(context);
+        return packageInfo != null ? packageInfo.versionCode : -1;
+    }
+
+    /**
+     * 获取应用的VersionName
+     *
+     * @param context
+     * @return
+     */
+    public static String getVersionName(Context context) {
+        PackageInfo packageInfo = getPackageInfo(context);
+        return packageInfo != null ? packageInfo.versionName : "";
+    }
+
+    /**
+     * 比较两个版本号
+     *
+     * @param versionName1
+     * @param versionName2
+     * @return [> 0 versionName1 > versionName2] [= 0 versionName1 = versionName2]  [< 0 versionName1 < versionName2]
+     */
+    public static int compareVersionName(@NonNull String versionName1, @NonNull String versionName2) {
+        if (versionName1.equals(versionName2)) {
+            return 0;
+        }
+        String[] versionArray1 = versionName1.split("\\.");//注意此处为正则匹配,不能用".";
+        String[] versionArray2 = versionName2.split("\\.");
+        int idx = 0;
+        int minLength = Math.min(versionArray1.length, versionArray2.length);//取最小长度值
+        int diff = 0;
+        while (idx < minLength
+                && (diff = versionArray1[idx].length() - versionArray2[idx].length()) == 0//先比较长度
+                && (diff = versionArray1[idx].compareTo(versionArray2[idx])) == 0) {//再比较字符
+            ++idx;
+        }
+        //如果已经分出大小,则直接返回,如果未分出大小,则再比较位数,有子版本的为大;
+        diff = (diff != 0) ? diff : versionArray1.length - versionArray2.length;
+        return diff;
+    }
+
+    //=============显示====================//
+
+    public static int dip2px(int dip, Context context) {
+        return (int) (dip * getDensity(context) + 0.5f);
+    }
+
+    private static float getDensity(Context context) {
+        return getDisplayMetrics(context).density;
+    }
+
+    private static DisplayMetrics getDisplayMetrics(Context context) {
+        return context.getResources().getDisplayMetrics();
+    }
+
+    /**
+     * Drawable to bitmap.
+     *
+     * @param drawable The drawable.
+     * @return bitmap
+     */
+    public static Bitmap drawable2Bitmap(final Drawable drawable) {
+        if (drawable instanceof BitmapDrawable) {
+            BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+            if (bitmapDrawable.getBitmap() != null) {
+                return bitmapDrawable.getBitmap();
+            }
+        }
+        Bitmap bitmap;
+        if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
+            bitmap = Bitmap.createBitmap(1, 1,
+                    drawable.getOpacity() != PixelFormat.OPAQUE
+                            ? Bitmap.Config.ARGB_8888
+                            : Bitmap.Config.RGB_565);
+        } else {
+            bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(),
+                    drawable.getIntrinsicHeight(),
+                    drawable.getOpacity() != PixelFormat.OPAQUE
+                            ? Bitmap.Config.ARGB_8888
+                            : Bitmap.Config.RGB_565);
+        }
+        Canvas canvas = new Canvas(bitmap);
+        drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+        drawable.draw(canvas);
+        return bitmap;
+    }
+
+    private static SharedPreferences getSP(Context context) {
+        return context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE);
+    }
+
+    /**
+     * 保存忽略的版本信息
+     *
+     * @param context 上下文
+     * @param newVersion 新版本
+     */
+    public static void saveIgnoreVersion(Context context, String newVersion) {
+        getSP(context).edit().putString(IGNORE_VERSION, newVersion).apply();
+    }
+
+    /**
+     * 是否是忽略版本
+     *
+     * @param context 上下文
+     * @param newVersion 新版本
+     * @return 是否是忽略版本
+     */
+    public static boolean isIgnoreVersion(Context context, String newVersion) {
+        return getSP(context).getString(IGNORE_VERSION, "").equals(newVersion);
+    }
+
+    /**
+     * 获取版本更新展示信息
+     *
+     * @param updateEntity
+     * @return
+     */
+    @NonNull
+    public static String getDisplayUpdateInfo(Context context, @NonNull UpdateEntity updateEntity) {
+        String targetSize = byte2FitMemorySize(updateEntity.getSize() * 1024);
+        final String updateContent = updateEntity.getUpdateContent();
+
+        String updateInfo = "";
+        if (!TextUtils.isEmpty(targetSize)) {
+            updateInfo = context.getString(R.string.xupdate_lab_new_version_size) + targetSize + "\n";
+        }
+        if (!TextUtils.isEmpty(updateContent)) {
+            updateInfo += updateContent;
+        }
+        return updateInfo;
+    }
+
+    /**
+     * 字节数转合适内存大小
+     * <p>保留 1 位小数</p>
+     *
+     * @param byteNum 字节数
+     * @return 合适内存大小
+     */
+    @SuppressLint("DefaultLocale")
+    private static String byte2FitMemorySize(final long byteNum) {
+        if (byteNum <= 0) {
+            return "";
+        } else if (byteNum < 1024) {
+            return String.format("%.1fB", (double) byteNum);
+        } else if (byteNum < 1048576) {
+            return String.format("%.1fKB", (double) byteNum / 1024);
+        } else if (byteNum < 1073741824) {
+            return String.format("%.1fMB", (double) byteNum / 1048576);
+        } else {
+            return String.format("%.1fGB", (double) byteNum / 1073741824);
+        }
+    }
+
+    //=============下载====================//
+
+    /**
+     * 判断更新的安装包是否已下载完成【比较md5值】
+     *
+     * @param updateEntity 更新信息
+     * @return
+     */
+    public static boolean isApkDownloaded(UpdateEntity updateEntity) {
+        File appFile = getApkFileByUpdateEntity(updateEntity);
+        return !TextUtils.isEmpty(updateEntity.getMd5())
+                && FileUtils.isFileExists(appFile)
+                && _XUpdate.isFileValid(updateEntity.getMd5(), appFile);
+    }
+
+    /**
+     * 根据更新信息获取apk安装文件
+     *
+     * @param updateEntity 更新信息
+     * @return
+     */
+    public static File getApkFileByUpdateEntity(UpdateEntity updateEntity) {
+        String appName = getApkNameByDownloadUrl(updateEntity.getDownloadUrl());
+        return new File(updateEntity.getApkCacheDir()
+                .concat(File.separator + updateEntity.getVersionName())
+                .concat(File.separator + appName));
+    }
+
+    /**
+     * 根据下载地址获取文件名
+     *
+     * @param downloadUrl
+     * @return
+     */
+    @NonNull
+    public static String getApkNameByDownloadUrl(String downloadUrl) {
+        if (TextUtils.isEmpty(downloadUrl)) {
+            return "temp_" + System.currentTimeMillis() + ".apk";
+        } else {
+            String appName = downloadUrl.substring(downloadUrl.lastIndexOf("/") + 1);
+            if (!appName.endsWith(".apk")) {
+                appName = "temp_" + System.currentTimeMillis() + ".apk";
+            }
+            return appName;
+        }
+    }
+
+    /**
+     * 获取应用的缓存目录
+     *
+     * @param uniqueName 缓存目录
+     */
+    public static String getDiskCacheDir(Context context, String uniqueName) {
+        String cachePath;
+        if (isSDCardEnable() && context.getExternalCacheDir() != null) {
+            cachePath = context.getExternalCacheDir().getPath();
+        } else {
+            cachePath = context.getCacheDir().getPath();
+        }
+        return cachePath + File.separator + uniqueName;
+    }
+
+    /**
+     * @return 版本更新的默认缓存路径
+     */
+    public static File getDefaultDiskCacheDir() {
+        return FileUtils.getFileByPath(getDefaultDiskCacheDirPath());
+    }
+
+    /**
+     * ApkCacheDir是否是私有目录
+     *
+     * @param updateEntity 版本更新信息实体
+     * @return
+     */
+    public static boolean isPrivateApkCacheDir(@NonNull UpdateEntity updateEntity) {
+        return FileUtils.isPrivatePath(XUpdate.getContext(), updateEntity.getApkCacheDir());
+    }
+
+    /**
+     * @return 版本更新的默认缓存路径
+     */
+    public static String getDefaultDiskCacheDirPath() {
+        return UpdateUtils.getDiskCacheDir(XUpdate.getContext(), KEY_XUPDATE);
+    }
+
+    private static boolean isSDCardEnable() {
+        return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
+                || !Environment.isExternalStorageRemovable();
+    }
+
+    private static PackageInfo getPackageInfo(Context context) {
+        try {
+            return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
+        } catch (PackageManager.NameNotFoundException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public static String getAppName(Context context) {
+        PackageInfo packageInfo = getPackageInfo(context);
+        return packageInfo != null ? packageInfo.applicationInfo.loadLabel(context.getPackageManager()).toString() : "";
+    }
+
+    public static Drawable getAppIcon(Context context) {
+        PackageInfo packageInfo = getPackageInfo(context);
+        return packageInfo != null ? packageInfo.applicationInfo.loadIcon(context.getPackageManager()) : null;
+    }
+
+    /**
+     * 应用是否在前台
+     *
+     * @param context
+     * @return
+     */
+    public static boolean isAppOnForeground(Context context) {
+        ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+        String packageName = context.getPackageName();
+        List<ActivityManager.RunningAppProcessInfo> appProcesses = activityManager.getRunningAppProcesses();
+        if (appProcesses == null) {
+            return false;
+        }
+        for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
+            if (appProcess.processName.equals(packageName) && appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 是否是主线程
+     *
+     * @return 是否是主线程
+     */
+    public static boolean isMainThread() {
+        return Looper.getMainLooper() == Looper.myLooper();
+    }
+
+    /**
+     * 页面跳转
+     *
+     * @param intent 跳转意图
+     */
+    public static boolean startActivity(final Intent intent) {
+        if (intent == null) {
+            UpdateLog.e("[startActivity failed]: intent == null");
+            return false;
+        }
+        if (XUpdate.getContext().getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
+            try {
+                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                XUpdate.getContext().startActivity(intent);
+                return true;
+            } catch (ActivityNotFoundException e) {
+                e.printStackTrace();
+                UpdateLog.e(e);
+            }
+        } else {
+            UpdateLog.e("[resolveActivity failed]: " + (intent.getComponent() != null ? intent.getComponent().getClassName() : intent.getAction()) + " do not register in manifest");
+        }
+        return false;
+    }
+}

+ 185 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/widget/BaseDialog.java

@@ -0,0 +1,185 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.widget;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+import com.xuexiang.xupdate.R;
+import com.xuexiang.xupdate.utils.DialogUtils;
+
+import androidx.annotation.IdRes;
+import androidx.core.content.ContextCompat;
+
+/**
+ * 基类Dialog
+ * 触摸Dialog屏幕以外的区域,dialog消失同时隐藏键盘
+ *
+ * @author xuexiang
+ * @since 2018/7/24 上午9:34
+ */
+public abstract class BaseDialog extends Dialog {
+
+    private View mContentView;
+
+    /**
+     * 是否同步系统控制器显示状态,默认false【状态栏、三键导航栏等】
+     */
+    private boolean mIsSyncSystemUiVisibility;
+
+    public BaseDialog(Context context, int layoutId) {
+        this(context, R.style.XUpdate_Dialog, layoutId);
+    }
+
+    public BaseDialog(Context context, View contentView) {
+        this(context, R.style.XUpdate_Dialog, contentView);
+    }
+
+    public BaseDialog(Context context) {
+        super(context, R.style.XUpdate_Dialog);
+    }
+
+    public BaseDialog(Context context, int theme, int layoutId) {
+        super(context, theme);
+        init(layoutId);
+
+    }
+
+    public BaseDialog(Context context, int theme, View contentView) {
+        super(context, theme);
+        init(contentView);
+    }
+
+    private void init(int layoutId) {
+        View view = getLayoutInflater().inflate(layoutId, null);
+        init(view);
+    }
+
+    private void init(View view) {
+        setContentView(view);
+        mContentView = view;
+        setCanceledOnTouchOutside(true);
+
+        initViews();
+        initListeners();
+    }
+
+    @Override
+    public <T extends View> T findViewById(@IdRes int id) {
+        return mContentView.findViewById(id);
+    }
+
+    /**
+     * 初始化控件
+     */
+    protected abstract void initViews();
+
+    /**
+     * 初始化监听器
+     */
+    protected abstract void initListeners();
+
+    /**
+     * 设置弹窗的宽和高
+     *
+     * @param width  宽
+     * @param height 高
+     */
+    protected BaseDialog setDialogSize(int width, int height) {
+        // 获取对话框当前的参数值
+        Window window = getWindow();
+        if (window != null) {
+            WindowManager.LayoutParams p = getWindow().getAttributes();
+            p.width = width;
+            p.height = height;
+            window.setAttributes(p);
+        }
+        return this;
+    }
+
+    protected String getString(int resId) {
+        return getContext().getResources().getString(resId);
+    }
+
+    protected Drawable getDrawable(int resId) {
+        return ContextCompat.getDrawable(getContext(), resId);
+    }
+
+    /**
+     * 设置是否同步系统控制器显示状态
+     *
+     * @param isSyncSystemUiVisibility 是否同步系统控制器显示状态
+     * @return this
+     */
+    public BaseDialog setIsSyncSystemUiVisibility(boolean isSyncSystemUiVisibility) {
+        mIsSyncSystemUiVisibility = isSyncSystemUiVisibility;
+        return this;
+    }
+
+    /**
+     * 显示加载
+     */
+    @Override
+    public void show() {
+        showIfSync(mIsSyncSystemUiVisibility);
+    }
+
+    /**
+     * 显示弹窗,是否同步系统控制器显示状态
+     *
+     * @param isSyncSystemUiVisibility 是否同步系统控制器显示状态
+     */
+    public void showIfSync(boolean isSyncSystemUiVisibility) {
+        if (isSyncSystemUiVisibility) {
+            boolean isHandled = DialogUtils.showWindow(DialogUtils.findActivity(getContext()), getWindow(), new DialogUtils.IWindowShower() {
+                @Override
+                public void show(Window window) {
+                    performShow();
+                }
+            });
+            if (!isHandled) {
+                performShow();
+            }
+        } else {
+            performShow();
+        }
+    }
+
+    /**
+     * 真正执行显示的方法
+     */
+    protected void performShow() {
+        super.show();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
+            if (DialogUtils.isShouldHideInput(getWindow(), ev)) {
+                DialogUtils.hideSoftInput(getCurrentFocus());
+            }
+        }
+        return super.onTouchEvent(ev);
+    }
+
+
+}

+ 56 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/widget/IDownloadEventHandler.java

@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2020 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.xuexiang.xupdate.widget;
+
+import java.io.File;
+
+/**
+ * 下载事件处理者
+ *
+ * @author xuexiang
+ * @since 2020/12/23 10:47 PM
+ */
+public interface IDownloadEventHandler {
+
+    /**
+     * 处理开始下载
+     */
+    void handleStart();
+
+    /**
+     * 处理下载中的进度更新
+     *
+     * @param progress 下载进度
+     */
+    void handleProgress(float progress);
+
+    /**
+     * 处理下载完毕
+     *
+     * @param file 下载文件
+     * @return 下载完毕后是否打开文件进行安装<br>{@code true} :安装<br>{@code false} :不安装
+     */
+    boolean handleCompleted(File file);
+
+    /**
+     * 处理下载失败
+     *
+     * @param throwable 失败原因
+     */
+    void handleError(Throwable throwable);
+}

+ 515 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/widget/NumberProgressBar.java

@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.widget;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.xuexiang.xupdate.R;
+
+/**
+ * 数字进度条
+ *
+ * @author xuexiang
+ * @since 2018/7/2 上午11:23
+ */
+public class NumberProgressBar extends View {
+
+    /**
+     * For save and restore instance of progressbar.
+     */
+    private static final String INSTANCE_STATE = "saved_instance";
+    private static final String INSTANCE_TEXT_COLOR = "text_color";
+    private static final String INSTANCE_TEXT_SIZE = "text_size";
+    private static final String INSTANCE_REACHED_BAR_HEIGHT = "reached_bar_height";
+    private static final String INSTANCE_REACHED_BAR_COLOR = "reached_bar_color";
+    private static final String INSTANCE_UNREACHED_BAR_HEIGHT = "unreached_bar_height";
+    private static final String INSTANCE_UNREACHED_BAR_COLOR = "unreached_bar_color";
+    private static final String INSTANCE_MAX = "max";
+    private static final String INSTANCE_PROGRESS = "progress";
+    private static final String INSTANCE_SUFFIX = "suffix";
+    private static final String INSTANCE_PREFIX = "prefix";
+    private static final String INSTANCE_TEXT_VISIBILITY = "text_visibility";
+    private static final int PROGRESS_TEXT_VISIBLE = 0;
+    private int mMaxProgress = 100;
+    /**
+     * Current progress, can not exceed the max progress.
+     */
+    private int mCurrentProgress = 0;
+    /**
+     * The progress area bar color.
+     */
+    private int mReachedBarColor;
+    /**
+     * The bar unreached area color.
+     */
+    private int mUnreachedBarColor;
+    /**
+     * The progress text color.
+     */
+    private int mTextColor;
+    /**
+     * The progress text size.
+     */
+    private float mTextSize;
+    /**
+     * The height of the reached area.
+     */
+    private float mReachedBarHeight;
+    /**
+     * The height of the unreached area.
+     */
+    private float mUnreachedBarHeight;
+    /**
+     * The suffix of the number.
+     */
+    private String mSuffix = "%";
+    /**
+     * The prefix.
+     */
+    private String mPrefix = "";
+
+    /**
+     * The drawn text start.
+     */
+    private float mDrawTextStart;
+
+    /**
+     * The drawn text end.
+     */
+    private float mDrawTextEnd;
+
+    /**
+     * The text that to be drawn in onDraw().
+     */
+    private String mCurrentDrawText;
+
+    /**
+     * The Paint of the reached area.
+     */
+    private Paint mReachedBarPaint;
+    /**
+     * The Paint of the unreached area.
+     */
+    private Paint mUnreachedBarPaint;
+    /**
+     * The Paint of the progress text.
+     */
+    private Paint mTextPaint;
+
+    /**
+     * Unreached bar area to draw rect.
+     */
+    private RectF mUnreachedRectF = new RectF(0, 0, 0, 0);
+    /**
+     * Reached bar area rect.
+     */
+    private RectF mReachedRectF = new RectF(0, 0, 0, 0);
+
+    /**
+     * The progress text offset.
+     */
+    private float mOffset;
+
+    /**
+     * Determine if need to draw unreached area.
+     */
+    private boolean mDrawUnreachedBar = true;
+
+    private boolean mDrawReachedBar = true;
+
+    private boolean mIfDrawText = true;
+
+    /**
+     * 进度条监听
+     */
+    private OnProgressBarListener mListener;
+
+    public NumberProgressBar(Context context) {
+        this(context, null);
+    }
+
+    public NumberProgressBar(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public NumberProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        float defaultReachedBarHeight = dp2px(1.5f);
+        float defaultUnreachedBarHeight = dp2px(1.0f);
+        float defaultTextSize = sp2px(10);
+        float defaultProgressTextOffset = dp2px(3.0f);
+
+        //load styled attributes.
+        final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.XNumberProgressBar, defStyleAttr, 0);
+
+        int defaultReachedColor = Color.rgb(66, 145, 241);
+        mReachedBarColor = attributes.getColor(R.styleable.XNumberProgressBar_xnpb_reached_color, defaultReachedColor);
+        int defaultUnreachedColor = Color.rgb(204, 204, 204);
+        mUnreachedBarColor = attributes.getColor(R.styleable.XNumberProgressBar_xnpb_unreached_color, defaultUnreachedColor);
+        int defaultTextColor = Color.rgb(66, 145, 241);
+        mTextColor = attributes.getColor(R.styleable.XNumberProgressBar_xnpb_text_color, defaultTextColor);
+        mTextSize = attributes.getDimension(R.styleable.XNumberProgressBar_xnpb_text_size, defaultTextSize);
+
+        mReachedBarHeight = attributes.getDimension(R.styleable.XNumberProgressBar_xnpb_reached_bar_height, defaultReachedBarHeight);
+        mUnreachedBarHeight = attributes.getDimension(R.styleable.XNumberProgressBar_xnpb_unreached_bar_height, defaultUnreachedBarHeight);
+        mOffset = attributes.getDimension(R.styleable.XNumberProgressBar_xnpb_text_offset, defaultProgressTextOffset);
+
+        int textVisible = attributes.getInt(R.styleable.XNumberProgressBar_xnpb_text_visibility, PROGRESS_TEXT_VISIBLE);
+        if (textVisible != PROGRESS_TEXT_VISIBLE) {
+            mIfDrawText = false;
+        }
+
+        setProgress(attributes.getInt(R.styleable.XNumberProgressBar_xnpb_current, 0));
+        setMax(attributes.getInt(R.styleable.XNumberProgressBar_xnpb_max, 100));
+
+        attributes.recycle();
+        initializePainters();
+    }
+
+    @Override
+    protected int getSuggestedMinimumWidth() {
+        return (int) mTextSize;
+    }
+
+    @Override
+    protected int getSuggestedMinimumHeight() {
+        return Math.max((int) mTextSize, Math.max((int) mReachedBarHeight, (int) mUnreachedBarHeight));
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        setMeasuredDimension(measure(widthMeasureSpec, true), measure(heightMeasureSpec, false));
+    }
+
+    private int measure(int measureSpec, boolean isWidth) {
+        int result;
+        int mode = MeasureSpec.getMode(measureSpec);
+        int size = MeasureSpec.getSize(measureSpec);
+        int padding = isWidth ? getPaddingLeft() + getPaddingRight() : getPaddingTop() + getPaddingBottom();
+        if (mode == MeasureSpec.EXACTLY) {
+            result = size;
+        } else {
+            result = isWidth ? getSuggestedMinimumWidth() : getSuggestedMinimumHeight();
+            result += padding;
+            if (mode == MeasureSpec.AT_MOST) {
+                if (isWidth) {
+                    result = Math.max(result, size);
+                } else {
+                    result = Math.min(result, size);
+                }
+            }
+        }
+        return result;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (mIfDrawText) {
+            calculateDrawRectF();
+        } else {
+            calculateDrawRectFWithoutProgressText();
+        }
+
+        if (mDrawReachedBar) {
+            canvas.drawRect(mReachedRectF, mReachedBarPaint);
+        }
+
+        if (mDrawUnreachedBar) {
+            canvas.drawRect(mUnreachedRectF, mUnreachedBarPaint);
+        }
+
+        if (mIfDrawText) {
+            canvas.drawText(mCurrentDrawText, mDrawTextStart, mDrawTextEnd, mTextPaint);
+        }
+    }
+
+    private void initializePainters() {
+        mReachedBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mReachedBarPaint.setColor(mReachedBarColor);
+
+        mUnreachedBarPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mUnreachedBarPaint.setColor(mUnreachedBarColor);
+
+        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mTextPaint.setColor(mTextColor);
+        mTextPaint.setTextSize(mTextSize);
+
+    }
+
+    private void calculateDrawRectFWithoutProgressText() {
+        mReachedRectF.left = getPaddingLeft();
+        mReachedRectF.top = getHeight() / 2.0f - mReachedBarHeight / 2.0f;
+        mReachedRectF.right = (getWidth() - getPaddingLeft() - getPaddingRight()) / (getMax() * 1.0f) * getProgress() + getPaddingLeft();
+        mReachedRectF.bottom = getHeight() / 2.0f + mReachedBarHeight / 2.0f;
+
+        mUnreachedRectF.left = mReachedRectF.right;
+        mUnreachedRectF.right = getWidth() - getPaddingRight();
+        mUnreachedRectF.top = getHeight() / 2.0f + -mUnreachedBarHeight / 2.0f;
+        mUnreachedRectF.bottom = getHeight() / 2.0f + mUnreachedBarHeight / 2.0f;
+    }
+
+    @SuppressLint("DefaultLocale")
+    private void calculateDrawRectF() {
+        mCurrentDrawText = String.format("%d", getProgress() * 100 / getMax());
+        mCurrentDrawText = mPrefix + mCurrentDrawText + mSuffix;
+        /*
+         The width of the text that to be drawn.
+        */
+        float drawTextWidth = mTextPaint.measureText(mCurrentDrawText);
+
+        if (getProgress() == 0) {
+            mDrawReachedBar = false;
+            mDrawTextStart = getPaddingLeft();
+        } else {
+            mDrawReachedBar = true;
+            mReachedRectF.left = getPaddingLeft();
+            mReachedRectF.top = getHeight() / 2.0f - mReachedBarHeight / 2.0f;
+            mReachedRectF.right = (getWidth() - getPaddingLeft() - getPaddingRight()) / (getMax() * 1.0f) * getProgress() - mOffset + getPaddingLeft();
+            mReachedRectF.bottom = getHeight() / 2.0f + mReachedBarHeight / 2.0f;
+            mDrawTextStart = (mReachedRectF.right + mOffset);
+        }
+
+        mDrawTextEnd = (int) ((getHeight() / 2.0f) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2.0f));
+
+        if ((mDrawTextStart + drawTextWidth) >= getWidth() - getPaddingRight()) {
+            mDrawTextStart = getWidth() - getPaddingRight() - drawTextWidth;
+            mReachedRectF.right = mDrawTextStart - mOffset;
+        }
+
+        float unreachedBarStart = mDrawTextStart + drawTextWidth + mOffset;
+        if (unreachedBarStart >= getWidth() - getPaddingRight()) {
+            mDrawUnreachedBar = false;
+        } else {
+            mDrawUnreachedBar = true;
+            mUnreachedRectF.left = unreachedBarStart;
+            mUnreachedRectF.right = getWidth() - getPaddingRight();
+            mUnreachedRectF.top = getHeight() / 2.0f + -mUnreachedBarHeight / 2.0f;
+            mUnreachedRectF.bottom = getHeight() / 2.0f + mUnreachedBarHeight / 2.0f;
+        }
+    }
+
+    /**
+     * Get progress text color.
+     *
+     * @return progress text color.
+     */
+    public int getTextColor() {
+        return mTextColor;
+    }
+
+    /**
+     * Get progress text size.
+     *
+     * @return progress text size.
+     */
+    public float getProgressTextSize() {
+        return mTextSize;
+    }
+
+    public void setProgressTextSize(float textSize) {
+        mTextSize = textSize;
+        mTextPaint.setTextSize(mTextSize);
+        postInvalidate();
+    }
+
+    public int getUnreachedBarColor() {
+        return mUnreachedBarColor;
+    }
+
+    public void setUnreachedBarColor(int barColor) {
+        mUnreachedBarColor = barColor;
+        mUnreachedBarPaint.setColor(mUnreachedBarColor);
+        postInvalidate();
+    }
+
+    public int getReachedBarColor() {
+        return mReachedBarColor;
+    }
+
+    public void setReachedBarColor(int progressColor) {
+        mReachedBarColor = progressColor;
+        mReachedBarPaint.setColor(mReachedBarColor);
+        postInvalidate();
+    }
+
+    public int getProgress() {
+        return mCurrentProgress;
+    }
+
+    public void setProgress(int progress) {
+        if (progress <= getMax() && progress >= 0) {
+            mCurrentProgress = progress;
+            postInvalidate();
+        }
+    }
+
+    public int getMax() {
+        return mMaxProgress;
+    }
+
+    public void setMax(int maxProgress) {
+        if (maxProgress > 0) {
+            mMaxProgress = maxProgress;
+            postInvalidate();
+        }
+    }
+
+    public float getReachedBarHeight() {
+        return mReachedBarHeight;
+    }
+
+    public void setReachedBarHeight(float height) {
+        mReachedBarHeight = height;
+    }
+
+    public float getUnreachedBarHeight() {
+        return mUnreachedBarHeight;
+    }
+
+    public void setUnreachedBarHeight(float height) {
+        mUnreachedBarHeight = height;
+    }
+
+    public void setProgressTextColor(int textColor) {
+        this.mTextColor = textColor;
+        mTextPaint.setColor(mTextColor);
+        postInvalidate();
+    }
+
+    public String getSuffix() {
+        return mSuffix;
+    }
+
+    public void setSuffix(String suffix) {
+        if (suffix == null) {
+            mSuffix = "";
+        } else {
+            mSuffix = suffix;
+        }
+    }
+
+    public String getPrefix() {
+        return mPrefix;
+    }
+
+    public void setPrefix(String prefix) {
+        if (prefix == null) {
+            mPrefix = "";
+        } else {
+            mPrefix = prefix;
+        }
+    }
+
+    public void incrementProgressBy(int by) {
+        if (by > 0) {
+            setProgress(getProgress() + by);
+        }
+
+        if (mListener != null) {
+            mListener.onProgressChange(getProgress(), getMax());
+        }
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        final Bundle bundle = new Bundle();
+        bundle.putParcelable(INSTANCE_STATE, super.onSaveInstanceState());
+        bundle.putInt(INSTANCE_TEXT_COLOR, getTextColor());
+        bundle.putFloat(INSTANCE_TEXT_SIZE, getProgressTextSize());
+        bundle.putFloat(INSTANCE_REACHED_BAR_HEIGHT, getReachedBarHeight());
+        bundle.putFloat(INSTANCE_UNREACHED_BAR_HEIGHT, getUnreachedBarHeight());
+        bundle.putInt(INSTANCE_REACHED_BAR_COLOR, getReachedBarColor());
+        bundle.putInt(INSTANCE_UNREACHED_BAR_COLOR, getUnreachedBarColor());
+        bundle.putInt(INSTANCE_MAX, getMax());
+        bundle.putInt(INSTANCE_PROGRESS, getProgress());
+        bundle.putString(INSTANCE_SUFFIX, getSuffix());
+        bundle.putString(INSTANCE_PREFIX, getPrefix());
+        bundle.putBoolean(INSTANCE_TEXT_VISIBILITY, getProgressTextVisibility());
+        return bundle;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        if (state instanceof Bundle) {
+            final Bundle bundle = (Bundle) state;
+            mTextColor = bundle.getInt(INSTANCE_TEXT_COLOR);
+            mTextSize = bundle.getFloat(INSTANCE_TEXT_SIZE);
+            mReachedBarHeight = bundle.getFloat(INSTANCE_REACHED_BAR_HEIGHT);
+            mUnreachedBarHeight = bundle.getFloat(INSTANCE_UNREACHED_BAR_HEIGHT);
+            mReachedBarColor = bundle.getInt(INSTANCE_REACHED_BAR_COLOR);
+            mUnreachedBarColor = bundle.getInt(INSTANCE_UNREACHED_BAR_COLOR);
+            initializePainters();
+            setMax(bundle.getInt(INSTANCE_MAX));
+            setProgress(bundle.getInt(INSTANCE_PROGRESS));
+            setPrefix(bundle.getString(INSTANCE_PREFIX));
+            setSuffix(bundle.getString(INSTANCE_SUFFIX));
+            setProgressTextVisibility(bundle.getBoolean(INSTANCE_TEXT_VISIBILITY) ? ProgressTextVisibility.VISIBLE : ProgressTextVisibility.INVISIBLE);
+            super.onRestoreInstanceState(bundle.getParcelable(INSTANCE_STATE));
+            return;
+        }
+        super.onRestoreInstanceState(state);
+    }
+
+    public float dp2px(float dp) {
+        final float scale = getResources().getDisplayMetrics().density;
+        return dp * scale + 0.5f;
+    }
+
+    public float sp2px(float sp) {
+        final float scale = getResources().getDisplayMetrics().scaledDensity;
+        return sp * scale;
+    }
+
+    public boolean getProgressTextVisibility() {
+        return mIfDrawText;
+    }
+
+    public void setProgressTextVisibility(ProgressTextVisibility visibility) {
+        mIfDrawText = visibility == ProgressTextVisibility.VISIBLE;
+        postInvalidate();
+    }
+
+    public void setOnProgressBarListener(OnProgressBarListener listener) {
+        mListener = listener;
+    }
+
+    public enum ProgressTextVisibility {
+        VISIBLE, INVISIBLE
+    }
+
+    public interface OnProgressBarListener {
+        /**
+         * 进度变化
+         *
+         * @param current
+         * @param max
+         */
+        void onProgressChange(int current, int max);
+    }
+
+}

+ 445 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/widget/UpdateDialog.java

@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.widget;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.xuexiang.xupdate.R;
+import com.xuexiang.xupdate._XUpdate;
+import com.xuexiang.xupdate.entity.PromptEntity;
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.proxy.IPrompterProxy;
+import com.xuexiang.xupdate.utils.ColorUtils;
+import com.xuexiang.xupdate.utils.DrawableUtils;
+import com.xuexiang.xupdate.utils.UpdateUtils;
+
+import java.io.File;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.core.app.ActivityCompat;
+
+import static com.xuexiang.xupdate.widget.UpdateDialogFragment.REQUEST_CODE_REQUEST_PERMISSIONS;
+
+/**
+ * 版本更新弹窗
+ *
+ * @author xuexiang
+ * @since 2018/7/24 上午9:29
+ */
+public class UpdateDialog extends BaseDialog implements View.OnClickListener, IDownloadEventHandler {
+
+    //======顶部========//
+    /**
+     * 顶部图片
+     */
+    private ImageView mIvTop;
+    /**
+     * 标题
+     */
+    private TextView mTvTitle;
+    //======更新内容========//
+    /**
+     * 版本更新内容
+     */
+    private TextView mTvUpdateInfo;
+    /**
+     * 版本更新
+     */
+    private Button mBtnUpdate;
+    /**
+     * 后台更新
+     */
+    private Button mBtnBackgroundUpdate;
+    /**
+     * 忽略版本
+     */
+    private TextView mTvIgnore;
+    /**
+     * 进度条
+     */
+    private NumberProgressBar mNumberProgressBar;
+    //======底部========//
+    /**
+     * 底部关闭
+     */
+    private LinearLayout mLlClose;
+    private ImageView mIvClose;
+
+    //======更新信息========//
+    /**
+     * 更新信息
+     */
+    private UpdateEntity mUpdateEntity;
+    /**
+     * 更新代理
+     */
+    private IPrompterProxy mPrompterProxy;
+    /**
+     * 提示器参数信息
+     */
+    private PromptEntity mPromptEntity;
+
+    /**
+     * 获取更新提示
+     *
+     * @param updateEntity  更新信息
+     * @param prompterProxy 更新代理
+     * @param promptEntity  提示器参数信息
+     * @return 更新提示
+     */
+    public static UpdateDialog newInstance(@NonNull Context context, @NonNull UpdateEntity updateEntity, @NonNull IPrompterProxy prompterProxy, PromptEntity promptEntity) {
+        UpdateDialog dialog = new UpdateDialog(context);
+        dialog.setIPrompterProxy(prompterProxy)
+                .setUpdateEntity(updateEntity)
+                .setPromptEntity(promptEntity);
+        dialog.initTheme(promptEntity.getThemeColor(), promptEntity.getTopResId(), promptEntity.getButtonTextColor(), promptEntity.getWidthRatio(), promptEntity.getHeightRatio());
+        return dialog;
+    }
+
+    private UpdateDialog(Context context) {
+        super(context, R.layout.xupdate_dialog_update);
+    }
+
+    public UpdateDialog setPromptEntity(PromptEntity promptEntity) {
+        mPromptEntity = promptEntity;
+        return this;
+    }
+
+    @Override
+    protected void initViews() {
+        // 顶部图片
+        mIvTop = findViewById(R.id.iv_top);
+        // 标题
+        mTvTitle = findViewById(R.id.tv_title);
+        // 提示内容
+        mTvUpdateInfo = findViewById(R.id.tv_update_info);
+        // 更新按钮
+        mBtnUpdate = findViewById(R.id.btn_update);
+        // 后台更新按钮
+        mBtnBackgroundUpdate = findViewById(R.id.btn_background_update);
+        // 忽略
+        mTvIgnore = findViewById(R.id.tv_ignore);
+        // 进度条
+        mNumberProgressBar = findViewById(R.id.npb_progress);
+
+        // 关闭按钮+线 的整个布局
+        mLlClose = findViewById(R.id.ll_close);
+        // 关闭按钮
+        mIvClose = findViewById(R.id.iv_close);
+    }
+
+    @Override
+    protected void initListeners() {
+        mBtnUpdate.setOnClickListener(this);
+        mBtnBackgroundUpdate.setOnClickListener(this);
+        mIvClose.setOnClickListener(this);
+        mTvIgnore.setOnClickListener(this);
+
+        setCancelable(false);
+        setCanceledOnTouchOutside(false);
+        setIsSyncSystemUiVisibility(true);
+    }
+
+    //====================生命周期============================//
+
+    private String getUrl() {
+        return mPrompterProxy != null ? mPrompterProxy.getUrl() : "";
+    }
+
+    @Override
+    public void show() {
+        _XUpdate.setIsPrompterShow(getUrl(), true);
+        super.show();
+    }
+
+    @Override
+    public void dismiss() {
+        _XUpdate.setIsPrompterShow(getUrl(), false);
+        clearIPrompterProxy();
+        super.dismiss();
+    }
+
+    private void clearIPrompterProxy() {
+        if (mPrompterProxy != null) {
+            mPrompterProxy.recycle();
+            mPrompterProxy = null;
+        }
+    }
+    //====================UI构建============================//
+
+    public UpdateDialog setUpdateEntity(UpdateEntity updateEntity) {
+        mUpdateEntity = updateEntity;
+        initUpdateInfo(mUpdateEntity);
+        return this;
+    }
+
+    /**
+     * 初始化更新信息
+     *
+     * @param updateEntity 版本更新信息
+     */
+    private void initUpdateInfo(UpdateEntity updateEntity) {
+        // 弹出对话框
+        final String newVersion = updateEntity.getVersionName();
+        String updateInfo = UpdateUtils.getDisplayUpdateInfo(getContext(), updateEntity);
+        // 更新内容
+        mTvUpdateInfo.setText(updateInfo);
+        mTvTitle.setText(String.format(getString(R.string.xupdate_lab_ready_update), newVersion));
+
+        // 刷新升级按钮显示
+        refreshUpdateButton();
+
+        // 强制更新,不显示关闭按钮
+        if (updateEntity.isForce()) {
+            mLlClose.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * 初始化主题色
+     */
+    private void initTheme(@ColorInt int themeColor, @DrawableRes int topResId, @ColorInt int buttonTextColor, float widthRatio, float heightRatio) {
+        if (themeColor == -1) {
+            themeColor = ColorUtils.getColor(getContext(), R.color.xupdate_default_theme_color);
+        }
+        if (topResId == -1) {
+            topResId = R.drawable.xupdate_bg_app_top;
+        }
+        if (buttonTextColor == 0) {
+            buttonTextColor = ColorUtils.isColorDark(themeColor) ? Color.WHITE : Color.BLACK;
+        }
+        setDialogTheme(themeColor, topResId, buttonTextColor, widthRatio, heightRatio);
+    }
+
+    /**
+     * 设置⏏弹窗主题
+     *
+     * @param themeColor      主色
+     * @param topResId        图片
+     * @param buttonTextColor 按钮文字颜色
+     * @param widthRatio      宽和屏幕的比例
+     * @param heightRatio     高和屏幕的比例
+     */
+    private void setDialogTheme(int themeColor, int topResId, int buttonTextColor, float widthRatio, float heightRatio) {
+        Drawable topDrawable = _XUpdate.getTopDrawable(mPromptEntity.getTopDrawableTag());
+        if (topDrawable != null) {
+            mIvTop.setImageDrawable(topDrawable);
+        } else {
+            mIvTop.setImageResource(topResId);
+        }
+        DrawableUtils.setBackgroundCompat(mBtnUpdate, DrawableUtils.getDrawable(UpdateUtils.dip2px(4, getContext()), themeColor));
+        DrawableUtils.setBackgroundCompat(mBtnBackgroundUpdate, DrawableUtils.getDrawable(UpdateUtils.dip2px(4, getContext()), themeColor));
+        mNumberProgressBar.setProgressTextColor(themeColor);
+        mNumberProgressBar.setReachedBarColor(themeColor);
+        mBtnUpdate.setTextColor(buttonTextColor);
+        mBtnBackgroundUpdate.setTextColor(buttonTextColor);
+
+        initWindow(widthRatio, heightRatio);
+    }
+
+    private void initWindow(float widthRatio, float heightRatio) {
+        Window window = getWindow();
+        if (window == null) {
+            return;
+        }
+        WindowManager.LayoutParams lp = window.getAttributes();
+        DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics();
+        if (widthRatio > 0 && widthRatio < 1) {
+            lp.width = (int) (displayMetrics.widthPixels * widthRatio);
+        }
+        if (heightRatio > 0 && heightRatio < 1) {
+            lp.height = (int) (displayMetrics.heightPixels * heightRatio);
+        }
+        window.setAttributes(lp);
+    }
+
+    //====================更新功能============================//
+
+    private UpdateDialog setIPrompterProxy(IPrompterProxy prompterProxy) {
+        mPrompterProxy = prompterProxy;
+        return this;
+    }
+
+    @Override
+    public void onClick(View view) {
+        int i = view.getId();
+        //点击版本升级按钮【下载apk】
+        if (i == R.id.btn_update) {
+            //权限判断是否有访问外部存储空间权限
+            int flag = ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
+            if (!UpdateUtils.isPrivateApkCacheDir(mUpdateEntity) && flag != PackageManager.PERMISSION_GRANTED) {
+                ActivityCompat.requestPermissions((Activity) getContext(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_REQUEST_PERMISSIONS);
+            } else {
+                installApp();
+            }
+        } else if (i == R.id.btn_background_update) {
+            //点击后台更新按钮
+            mPrompterProxy.backgroundDownload();
+            dismiss();
+        } else if (i == R.id.iv_close) {
+            //点击关闭按钮
+            mPrompterProxy.cancelDownload();
+            dismiss();
+        } else if (i == R.id.tv_ignore) {
+            //点击忽略按钮
+            UpdateUtils.saveIgnoreVersion(getContext(), mUpdateEntity.getVersionName());
+            dismiss();
+        }
+    }
+
+    private void installApp() {
+        if (UpdateUtils.isApkDownloaded(mUpdateEntity)) {
+            onInstallApk();
+            //安装完自杀
+            //如果上次是强制更新,但是用户在下载完,强制杀掉后台,重新启动app后,则会走到这一步,所以要进行强制更新的判断。
+            if (!mUpdateEntity.isForce()) {
+                dismiss();
+            } else {
+                showInstallButton();
+            }
+        } else {
+            if (mPrompterProxy != null) {
+                mPrompterProxy.startDownload(mUpdateEntity, new WeakFileDownloadListener(this));
+            }
+            //忽略版本在点击更新按钮后隐藏
+            if (mUpdateEntity.isIgnorable()) {
+                mTvIgnore.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    @Override
+    public void handleStart() {
+        if (isShowing()) {
+            doStart();
+        }
+    }
+
+    private void doStart() {
+        mNumberProgressBar.setVisibility(View.VISIBLE);
+        mNumberProgressBar.setProgress(0);
+        mBtnUpdate.setVisibility(View.GONE);
+        if (mPromptEntity.isSupportBackgroundUpdate()) {
+            mBtnBackgroundUpdate.setVisibility(View.VISIBLE);
+        } else {
+            mBtnBackgroundUpdate.setVisibility(View.GONE);
+        }
+    }
+
+    @Override
+    public void handleProgress(float progress) {
+        if (isShowing()) {
+            if (mNumberProgressBar.getVisibility() == View.GONE) {
+                doStart();
+            }
+            mNumberProgressBar.setProgress(Math.round(progress * 100));
+            mNumberProgressBar.setMax(100);
+        }
+    }
+
+    @Override
+    public boolean handleCompleted(File file) {
+        if (isShowing()) {
+            mBtnBackgroundUpdate.setVisibility(View.GONE);
+            if (mUpdateEntity.isForce()) {
+                showInstallButton();
+            } else {
+                dismiss();
+            }
+        }
+        // 返回true,自动进行apk安装
+        return true;
+    }
+
+    @Override
+    public void handleError(Throwable throwable) {
+        if (isShowing()) {
+            if (mPromptEntity.isIgnoreDownloadError()) {
+                refreshUpdateButton();
+            } else {
+                dismiss();
+            }
+        }
+    }
+
+    /**
+     * 刷新升级按钮显示
+     */
+    private void refreshUpdateButton() {
+        if (UpdateUtils.isApkDownloaded(mUpdateEntity)) {
+            showInstallButton();
+        } else {
+            showUpdateButton();
+        }
+        mTvIgnore.setVisibility(mUpdateEntity.isIgnorable() ? View.VISIBLE : View.GONE);
+    }
+
+    /**
+     * 显示安装的按钮
+     */
+    private void showInstallButton() {
+        mNumberProgressBar.setVisibility(View.GONE);
+        mBtnBackgroundUpdate.setVisibility(View.GONE);
+        mBtnUpdate.setText(R.string.xupdate_lab_install);
+        mBtnUpdate.setVisibility(View.VISIBLE);
+        mBtnUpdate.setOnClickListener(this);
+    }
+
+    /**
+     * 显示升级的按钮
+     */
+    private void showUpdateButton() {
+        mNumberProgressBar.setVisibility(View.GONE);
+        mBtnBackgroundUpdate.setVisibility(View.GONE);
+        mBtnUpdate.setText(R.string.xupdate_lab_update);
+        mBtnUpdate.setVisibility(View.VISIBLE);
+        mBtnUpdate.setOnClickListener(this);
+    }
+
+    private void onInstallApk() {
+        _XUpdate.startInstallApk(getContext(), UpdateUtils.getApkFileByUpdateEntity(mUpdateEntity), mUpdateEntity.getDownLoadEntity());
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        _XUpdate.setIsPrompterShow(getUrl(), true);
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        _XUpdate.setIsPrompterShow(getUrl(), false);
+        clearIPrompterProxy();
+        super.onDetachedFromWindow();
+    }
+
+}

+ 479 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/widget/UpdateDialogActivity.java

@@ -0,0 +1,479 @@
+package com.xuexiang.xupdate.widget;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.xuexiang.xupdate.R;
+import com.xuexiang.xupdate._XUpdate;
+import com.xuexiang.xupdate.entity.PromptEntity;
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.proxy.IPrompterProxy;
+import com.xuexiang.xupdate.utils.ColorUtils;
+import com.xuexiang.xupdate.utils.DrawableUtils;
+import com.xuexiang.xupdate.utils.UpdateUtils;
+
+import java.io.File;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.app.ActivityCompat;
+
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.DOWNLOAD_PERMISSION_DENIED;
+import static com.xuexiang.xupdate.widget.UpdateDialogFragment.KEY_UPDATE_ENTITY;
+import static com.xuexiang.xupdate.widget.UpdateDialogFragment.KEY_UPDATE_PROMPT_ENTITY;
+import static com.xuexiang.xupdate.widget.UpdateDialogFragment.REQUEST_CODE_REQUEST_PERMISSIONS;
+
+/**
+ * 版本更新提示器【AppCompatActivity实现】
+ *
+ * @author xuexiang
+ * @since 2020/6/8 10:47 PM
+ */
+public class UpdateDialogActivity extends AppCompatActivity implements View.OnClickListener, IDownloadEventHandler {
+
+    //======顶部========//
+    /**
+     * 顶部图片
+     */
+    private ImageView mIvTop;
+    /**
+     * 标题
+     */
+    private TextView mTvTitle;
+    //======更新内容========//
+    /**
+     * 版本更新内容
+     */
+    private TextView mTvUpdateInfo;
+    /**
+     * 版本更新
+     */
+    private Button mBtnUpdate;
+    /**
+     * 后台更新
+     */
+    private Button mBtnBackgroundUpdate;
+    /**
+     * 忽略版本
+     */
+    private TextView mTvIgnore;
+    /**
+     * 进度条
+     */
+    private NumberProgressBar mNumberProgressBar;
+    //======底部========//
+    /**
+     * 底部关闭
+     */
+    private LinearLayout mLlClose;
+    private ImageView mIvClose;
+
+    //======更新信息========//
+    /**
+     * 更新信息
+     */
+    private UpdateEntity mUpdateEntity;
+    /**
+     * 更新代理
+     */
+    private static IPrompterProxy sIPrompterProxy;
+    /**
+     * 提示器参数信息
+     */
+    private PromptEntity mPromptEntity;
+
+    /**
+     * 显示更新提示
+     *
+     * @param updateEntity  更新信息
+     * @param prompterProxy 更新代理
+     * @param promptEntity  提示器参数信息
+     */
+    public static void show(@NonNull Context context, @NonNull UpdateEntity updateEntity, @NonNull IPrompterProxy prompterProxy, @NonNull PromptEntity promptEntity) {
+        Intent intent = new Intent(context, UpdateDialogActivity.class);
+        intent.putExtra(KEY_UPDATE_ENTITY, updateEntity);
+        intent.putExtra(KEY_UPDATE_PROMPT_ENTITY, promptEntity);
+        if (!(context instanceof Activity)) {
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        }
+        setIPrompterProxy(prompterProxy);
+        context.startActivity(intent);
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.xupdate_layout_update_prompter);
+        _XUpdate.setIsPrompterShow(getUrl(), true);
+        initView();
+        initData();
+    }
+
+    private void initView() {
+        // 顶部图片
+        mIvTop = findViewById(R.id.iv_top);
+        // 标题
+        mTvTitle = findViewById(R.id.tv_title);
+        // 提示内容
+        mTvUpdateInfo = findViewById(R.id.tv_update_info);
+        // 更新按钮
+        mBtnUpdate = findViewById(R.id.btn_update);
+        // 后台更新按钮
+        mBtnBackgroundUpdate = findViewById(R.id.btn_background_update);
+        // 忽略
+        mTvIgnore = findViewById(R.id.tv_ignore);
+        // 进度条
+        mNumberProgressBar = findViewById(R.id.npb_progress);
+
+        // 关闭按钮+线 的整个布局
+        mLlClose = findViewById(R.id.ll_close);
+        // 关闭按钮
+        mIvClose = findViewById(R.id.iv_close);
+    }
+
+    /**
+     * 初始化数据
+     */
+    private void initData() {
+        Bundle bundle = getIntent().getExtras();
+        if (bundle == null) {
+            return;
+        }
+        mPromptEntity = bundle.getParcelable(KEY_UPDATE_PROMPT_ENTITY);
+        // 设置主题色
+        if (mPromptEntity == null) {
+            // 如果不存在就使用默认的
+            mPromptEntity = new PromptEntity();
+        }
+        initTheme(mPromptEntity.getThemeColor(), mPromptEntity.getTopResId(), mPromptEntity.getButtonTextColor());
+        mUpdateEntity = bundle.getParcelable(KEY_UPDATE_ENTITY);
+        if (mUpdateEntity != null) {
+            initUpdateInfo(mUpdateEntity);
+            initListeners();
+        }
+    }
+
+    /**
+     * @return 版本更新提示器参数信息
+     */
+    private PromptEntity getPromptEntity() {
+        // 先从bundle中去取
+        if (mPromptEntity == null) {
+            Bundle bundle = getIntent().getExtras();
+            if (bundle != null) {
+                mPromptEntity = bundle.getParcelable(KEY_UPDATE_PROMPT_ENTITY);
+            }
+        }
+        // 如果还不存在就使用默认的
+        if (mPromptEntity == null) {
+            mPromptEntity = new PromptEntity();
+        }
+        return mPromptEntity;
+    }
+
+    /**
+     * 初始化更新信息
+     *
+     * @param updateEntity 更新信息
+     */
+    private void initUpdateInfo(UpdateEntity updateEntity) {
+        // 弹出对话框
+        final String newVersion = updateEntity.getVersionName();
+        String updateInfo = UpdateUtils.getDisplayUpdateInfo(this, updateEntity);
+        // 更新内容
+        mTvUpdateInfo.setText(updateInfo);
+        mTvTitle.setText(String.format(getString(R.string.xupdate_lab_ready_update), newVersion));
+
+        // 刷新升级按钮显示
+        refreshUpdateButton();
+
+        // 强制更新,不显示关闭按钮
+        if (updateEntity.isForce()) {
+            mLlClose.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * 初始化主题色
+     */
+    private void initTheme(@ColorInt int themeColor, @DrawableRes int topResId, @ColorInt int buttonTextColor) {
+        if (themeColor == -1) {
+            themeColor = ColorUtils.getColor(this, R.color.xupdate_default_theme_color);
+        }
+        if (topResId == -1) {
+            topResId = R.drawable.xupdate_bg_app_top;
+        }
+        if (buttonTextColor == 0) {
+            buttonTextColor = ColorUtils.isColorDark(themeColor) ? Color.WHITE : Color.BLACK;
+        }
+        setDialogTheme(themeColor, topResId, buttonTextColor);
+    }
+
+    /**
+     * 设置
+     *
+     * @param themeColor 主题色
+     * @param topResId   图片
+     */
+    private void setDialogTheme(int themeColor, int topResId, int buttonTextColor) {
+        Drawable topDrawable = _XUpdate.getTopDrawable(mPromptEntity.getTopDrawableTag());
+        if (topDrawable != null) {
+            mIvTop.setImageDrawable(topDrawable);
+        } else {
+            mIvTop.setImageResource(topResId);
+        }
+        DrawableUtils.setBackgroundCompat(mBtnUpdate, DrawableUtils.getDrawable(UpdateUtils.dip2px(4, this), themeColor));
+        DrawableUtils.setBackgroundCompat(mBtnBackgroundUpdate, DrawableUtils.getDrawable(UpdateUtils.dip2px(4, this), themeColor));
+        mNumberProgressBar.setProgressTextColor(themeColor);
+        mNumberProgressBar.setReachedBarColor(themeColor);
+        mBtnUpdate.setTextColor(buttonTextColor);
+        mBtnBackgroundUpdate.setTextColor(buttonTextColor);
+    }
+
+    private void initListeners() {
+        mBtnUpdate.setOnClickListener(this);
+        mBtnBackgroundUpdate.setOnClickListener(this);
+        mIvClose.setOnClickListener(this);
+        mTvIgnore.setOnClickListener(this);
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+        initWindowStyle();
+    }
+
+    private void initWindowStyle() {
+        Window window = getWindow();
+        if (window != null) {
+            PromptEntity promptEntity = getPromptEntity();
+            window.setGravity(Gravity.CENTER);
+            WindowManager.LayoutParams lp = window.getAttributes();
+            DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+            if (promptEntity.getWidthRatio() > 0 && promptEntity.getWidthRatio() < 1) {
+                lp.width = (int) (displayMetrics.widthPixels * promptEntity.getWidthRatio());
+            }
+            if (promptEntity.getHeightRatio() > 0 && promptEntity.getHeightRatio() < 1) {
+                lp.height = (int) (displayMetrics.heightPixels * promptEntity.getHeightRatio());
+            }
+            window.setAttributes(lp);
+        }
+    }
+
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        // 禁用返回键
+        return keyCode == KeyEvent.KEYCODE_BACK;
+    }
+
+    @Override
+    public void onClick(View view) {
+        int i = view.getId();
+        // 点击版本升级按钮【下载apk】
+        if (i == R.id.btn_update) {
+            // 权限判断是否有访问外部存储空间权限
+            int flag = ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE);
+            if (!UpdateUtils.isPrivateApkCacheDir(mUpdateEntity) && flag != PackageManager.PERMISSION_GRANTED) {
+                ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_REQUEST_PERMISSIONS);
+            } else {
+                installApp();
+            }
+        } else if (i == R.id.btn_background_update) {
+            // 点击后台更新按钮
+            if (sIPrompterProxy != null) {
+                sIPrompterProxy.backgroundDownload();
+            }
+            dismissDialog();
+        } else if (i == R.id.iv_close) {
+            // 点击关闭按钮
+            if (sIPrompterProxy != null) {
+                sIPrompterProxy.cancelDownload();
+            }
+            dismissDialog();
+        } else if (i == R.id.tv_ignore) {
+            // 点击忽略按钮
+            UpdateUtils.saveIgnoreVersion(this, mUpdateEntity.getVersionName());
+            dismissDialog();
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+        if (requestCode == REQUEST_CODE_REQUEST_PERMISSIONS) {
+            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                // 升级
+                installApp();
+            } else {
+                _XUpdate.onUpdateError(DOWNLOAD_PERMISSION_DENIED);
+                dismissDialog();
+            }
+        }
+
+    }
+
+    private void installApp() {
+        if (UpdateUtils.isApkDownloaded(mUpdateEntity)) {
+            onInstallApk();
+            // 安装完自杀
+            // 如果上次是强制更新,但是用户在下载完,强制杀掉后台,重新启动app后,则会走到这一步,所以要进行强制更新的判断。
+            if (!mUpdateEntity.isForce()) {
+                dismissDialog();
+            } else {
+                showInstallButton();
+            }
+        } else {
+            if (sIPrompterProxy != null) {
+                sIPrompterProxy.startDownload(mUpdateEntity, new WeakFileDownloadListener(this));
+            }
+            // 忽略版本在点击更新按钮后隐藏
+            if (mUpdateEntity.isIgnorable()) {
+                mTvIgnore.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    @Override
+    public void handleStart() {
+        if (!isFinishing()) {
+            doStart();
+        }
+    }
+
+    private void doStart() {
+        mNumberProgressBar.setVisibility(View.VISIBLE);
+        mNumberProgressBar.setProgress(0);
+        mBtnUpdate.setVisibility(View.GONE);
+        if (mPromptEntity.isSupportBackgroundUpdate()) {
+            mBtnBackgroundUpdate.setVisibility(View.VISIBLE);
+        } else {
+            mBtnBackgroundUpdate.setVisibility(View.GONE);
+        }
+    }
+
+    @Override
+    public void handleProgress(float progress) {
+        if (!isFinishing()) {
+            if (mNumberProgressBar.getVisibility() == View.GONE) {
+                doStart();
+            }
+            mNumberProgressBar.setProgress(Math.round(progress * 100));
+            mNumberProgressBar.setMax(100);
+        }
+    }
+
+    @Override
+    public boolean handleCompleted(File file) {
+        if (!isFinishing()) {
+            mBtnBackgroundUpdate.setVisibility(View.GONE);
+            if (mUpdateEntity.isForce()) {
+                showInstallButton();
+            } else {
+                dismissDialog();
+            }
+        }
+        // 返回true,自动进行apk安装
+        return true;
+    }
+
+    @Override
+    public void handleError(Throwable throwable) {
+        if (!isFinishing()) {
+            if (mPromptEntity.isIgnoreDownloadError()) {
+                refreshUpdateButton();
+            } else {
+                dismissDialog();
+            }
+        }
+    }
+
+    /**
+     * 刷新升级按钮显示
+     */
+    private void refreshUpdateButton() {
+        if (UpdateUtils.isApkDownloaded(mUpdateEntity)) {
+            showInstallButton();
+        } else {
+            showUpdateButton();
+        }
+        mTvIgnore.setVisibility(mUpdateEntity.isIgnorable() ? View.VISIBLE : View.GONE);
+    }
+
+    /**
+     * 显示安装的按钮
+     */
+    private void showInstallButton() {
+        mNumberProgressBar.setVisibility(View.GONE);
+        mBtnBackgroundUpdate.setVisibility(View.GONE);
+        mBtnUpdate.setText(R.string.xupdate_lab_install);
+        mBtnUpdate.setVisibility(View.VISIBLE);
+        mBtnUpdate.setOnClickListener(this);
+    }
+
+    /**
+     * 显示升级的按钮
+     */
+    private void showUpdateButton() {
+        mNumberProgressBar.setVisibility(View.GONE);
+        mBtnBackgroundUpdate.setVisibility(View.GONE);
+        mBtnUpdate.setText(R.string.xupdate_lab_update);
+        mBtnUpdate.setVisibility(View.VISIBLE);
+        mBtnUpdate.setOnClickListener(this);
+    }
+
+    private void onInstallApk() {
+        _XUpdate.startInstallApk(this, UpdateUtils.getApkFileByUpdateEntity(mUpdateEntity), mUpdateEntity.getDownLoadEntity());
+    }
+
+    /**
+     * 弹窗消失
+     */
+    private void dismissDialog() {
+        finish();
+    }
+
+    @Override
+    protected void onStop() {
+        if (isFinishing()) {
+            _XUpdate.setIsPrompterShow(getUrl(), false);
+            clearIPrompterProxy();
+        }
+        super.onStop();
+    }
+
+    private static void setIPrompterProxy(IPrompterProxy prompterProxy) {
+        UpdateDialogActivity.sIPrompterProxy = prompterProxy;
+    }
+
+    private static void clearIPrompterProxy() {
+        if (sIPrompterProxy != null) {
+            sIPrompterProxy.recycle();
+            sIPrompterProxy = null;
+        }
+    }
+
+    private String getUrl() {
+        return sIPrompterProxy != null ? sIPrompterProxy.getUrl() : "";
+    }
+
+}

+ 572 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/widget/UpdateDialogFragment.java

@@ -0,0 +1,572 @@
+/*
+ * Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.xuexiang.xupdate.widget;
+
+import android.Manifest;
+import android.app.Dialog;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.graphics.Color;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.xuexiang.xupdate.R;
+import com.xuexiang.xupdate._XUpdate;
+import com.xuexiang.xupdate.entity.PromptEntity;
+import com.xuexiang.xupdate.entity.UpdateEntity;
+import com.xuexiang.xupdate.proxy.IPrompterProxy;
+import com.xuexiang.xupdate.utils.ColorUtils;
+import com.xuexiang.xupdate.utils.DialogUtils;
+import com.xuexiang.xupdate.utils.DrawableUtils;
+import com.xuexiang.xupdate.utils.UpdateUtils;
+
+import java.io.File;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.app.ActivityCompat;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentManager;
+
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.DOWNLOAD_PERMISSION_DENIED;
+import static com.xuexiang.xupdate.entity.UpdateError.ERROR.PROMPT_UNKNOWN;
+
+/**
+ * 版本更新提示器【DialogFragment实现】
+ *
+ * @author xuexiang
+ * @since 2018/7/2 上午11:40
+ */
+public class UpdateDialogFragment extends DialogFragment implements View.OnClickListener, IDownloadEventHandler {
+    public final static String KEY_UPDATE_ENTITY = "key_update_entity";
+    public final static String KEY_UPDATE_PROMPT_ENTITY = "key_update_prompt_entity";
+
+    public final static int REQUEST_CODE_REQUEST_PERMISSIONS = 111;
+
+    //======顶部========//
+    /**
+     * 顶部图片
+     */
+    private ImageView mIvTop;
+    /**
+     * 标题
+     */
+    private TextView mTvTitle;
+    //======更新内容========//
+    /**
+     * 版本更新内容
+     */
+    private TextView mTvUpdateInfo;
+    /**
+     * 版本更新
+     */
+    private Button mBtnUpdate;
+    /**
+     * 后台更新
+     */
+    private Button mBtnBackgroundUpdate;
+    /**
+     * 忽略版本
+     */
+    private TextView mTvIgnore;
+    /**
+     * 进度条
+     */
+    private NumberProgressBar mNumberProgressBar;
+    //======底部========//
+    /**
+     * 底部关闭
+     */
+    private LinearLayout mLlClose;
+    private ImageView mIvClose;
+
+    //======更新信息========//
+    /**
+     * 更新信息
+     */
+    private UpdateEntity mUpdateEntity;
+    /**
+     * 更新代理
+     */
+    private static IPrompterProxy sIPrompterProxy;
+    /**
+     * 提示器参数信息
+     */
+    private PromptEntity mPromptEntity;
+    /**
+     * 当前屏幕方向
+     */
+    private int mCurrentOrientation;
+
+    /**
+     * 获取更新提示
+     *
+     * @param fragmentManager fragment管理者
+     * @param updateEntity    更新信息
+     * @param prompterProxy   更新代理
+     * @param promptEntity    提示器参数信息
+     */
+    public static void show(@NonNull FragmentManager fragmentManager, @NonNull UpdateEntity updateEntity, @NonNull IPrompterProxy prompterProxy, @NonNull PromptEntity promptEntity) {
+        UpdateDialogFragment fragment = new UpdateDialogFragment();
+        Bundle args = new Bundle();
+        args.putParcelable(KEY_UPDATE_ENTITY, updateEntity);
+        args.putParcelable(KEY_UPDATE_PROMPT_ENTITY, promptEntity);
+        fragment.setArguments(args);
+        setIPrompterProxy(prompterProxy);
+        fragment.show(fragmentManager);
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        _XUpdate.setIsPrompterShow(getUrl(), true);
+        setStyle(DialogFragment.STYLE_NO_TITLE, R.style.XUpdate_Fragment_Dialog);
+        mCurrentOrientation = getResources().getConfiguration().orientation;
+    }
+
+    @Override
+    public void onStart() {
+        Dialog dialog = getDialog();
+        if (dialog == null) {
+            return;
+        }
+        Window window = dialog.getWindow();
+        if (window == null) {
+            return;
+        }
+        window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+        // 在super.onStart();中调用mDialog.show
+        super.onStart();
+        DialogUtils.syncSystemUiVisibility(getActivity(), window);
+        window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+        initDialog();
+    }
+
+    private void initDialog() {
+        Dialog dialog = getDialog();
+        if (dialog == null) {
+            return;
+        }
+        dialog.setCanceledOnTouchOutside(false);
+        setCancelable(false);
+        Window window = dialog.getWindow();
+        if (window == null) {
+            return;
+        }
+        PromptEntity promptEntity = getPromptEntity();
+        window.setGravity(Gravity.CENTER);
+        WindowManager.LayoutParams lp = window.getAttributes();
+        DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+        if (promptEntity.getWidthRatio() > 0 && promptEntity.getWidthRatio() < 1) {
+            lp.width = (int) (displayMetrics.widthPixels * promptEntity.getWidthRatio());
+        }
+        if (promptEntity.getHeightRatio() > 0 && promptEntity.getHeightRatio() < 1) {
+            lp.height = (int) (displayMetrics.heightPixels * promptEntity.getHeightRatio());
+        }
+        window.setAttributes(lp);
+    }
+
+
+    @Nullable
+    @Override
+    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+        return inflater.inflate(R.layout.xupdate_layout_update_prompter, container);
+    }
+
+    @Override
+    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+        super.onViewCreated(view, savedInstanceState);
+        initView(view);
+        initData();
+    }
+
+    private void initView(View view) {
+        // 顶部图片
+        mIvTop = view.findViewById(R.id.iv_top);
+        // 标题
+        mTvTitle = view.findViewById(R.id.tv_title);
+        // 提示内容
+        mTvUpdateInfo = view.findViewById(R.id.tv_update_info);
+        // 更新按钮
+        mBtnUpdate = view.findViewById(R.id.btn_update);
+        // 后台更新按钮
+        mBtnBackgroundUpdate = view.findViewById(R.id.btn_background_update);
+        // 忽略
+        mTvIgnore = view.findViewById(R.id.tv_ignore);
+        // 进度条
+        mNumberProgressBar = view.findViewById(R.id.npb_progress);
+
+        // 关闭按钮+线 的整个布局
+        mLlClose = view.findViewById(R.id.ll_close);
+        // 关闭按钮
+        mIvClose = view.findViewById(R.id.iv_close);
+    }
+
+    /**
+     * 初始化数据
+     */
+    private void initData() {
+        Bundle bundle = getArguments();
+        if (bundle == null) {
+            return;
+        }
+        mPromptEntity = bundle.getParcelable(KEY_UPDATE_PROMPT_ENTITY);
+        // 设置主题色
+        if (mPromptEntity == null) {
+            // 如果不存在就使用默认的
+            mPromptEntity = new PromptEntity();
+        }
+        initTheme(mPromptEntity.getThemeColor(), mPromptEntity.getTopResId(), mPromptEntity.getButtonTextColor());
+        mUpdateEntity = bundle.getParcelable(KEY_UPDATE_ENTITY);
+        if (mUpdateEntity != null) {
+            initUpdateInfo(mUpdateEntity);
+            initListeners();
+        }
+    }
+
+    /**
+     * @return 版本更新提示器参数信息
+     */
+    private PromptEntity getPromptEntity() {
+        // 先从bundle中去取
+        if (mPromptEntity == null) {
+            Bundle bundle = getArguments();
+            if (bundle != null) {
+                mPromptEntity = bundle.getParcelable(KEY_UPDATE_PROMPT_ENTITY);
+            }
+        }
+        // 如果还不存在就使用默认的
+        if (mPromptEntity == null) {
+            mPromptEntity = new PromptEntity();
+        }
+        return mPromptEntity;
+    }
+
+    /**
+     * 初始化更新信息
+     *
+     * @param updateEntity 版本更新信息
+     */
+    private void initUpdateInfo(UpdateEntity updateEntity) {
+        // 弹出对话框
+        final String newVersion = updateEntity.getVersionName();
+        String updateInfo = UpdateUtils.getDisplayUpdateInfo(getContext(), updateEntity);
+        // 更新内容
+        mTvUpdateInfo.setText(updateInfo);
+        mTvTitle.setText(String.format(getString(R.string.xupdate_lab_ready_update), newVersion));
+
+        // 刷新升级按钮显示
+        refreshUpdateButton();
+        // 强制更新,不显示关闭按钮
+        if (updateEntity.isForce()) {
+            mLlClose.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * 初始化主题色
+     */
+    private void initTheme(@ColorInt int themeColor, @DrawableRes int topResId, @ColorInt int buttonTextColor) {
+        if (themeColor == -1) {
+            themeColor = ColorUtils.getColor(getContext(), R.color.xupdate_default_theme_color);
+        }
+        if (topResId == -1) {
+            topResId = R.drawable.xupdate_bg_app_top;
+        }
+        if (buttonTextColor == 0) {
+            buttonTextColor = ColorUtils.isColorDark(themeColor) ? Color.WHITE : Color.BLACK;
+        }
+        setDialogTheme(themeColor, topResId, buttonTextColor);
+    }
+
+    /**
+     * 设置
+     *
+     * @param themeColor 主题色
+     * @param topResId   图片
+     */
+    private void setDialogTheme(int themeColor, int topResId, int buttonTextColor) {
+        Drawable topDrawable = _XUpdate.getTopDrawable(mPromptEntity.getTopDrawableTag());
+        if (topDrawable != null) {
+            mIvTop.setImageDrawable(topDrawable);
+        } else {
+            mIvTop.setImageResource(topResId);
+        }
+        DrawableUtils.setBackgroundCompat(mBtnUpdate, DrawableUtils.getDrawable(UpdateUtils.dip2px(4, getContext()), themeColor));
+        DrawableUtils.setBackgroundCompat(mBtnBackgroundUpdate, DrawableUtils.getDrawable(UpdateUtils.dip2px(4, getContext()), themeColor));
+        mNumberProgressBar.setProgressTextColor(themeColor);
+        mNumberProgressBar.setReachedBarColor(themeColor);
+        mBtnUpdate.setTextColor(buttonTextColor);
+        mBtnBackgroundUpdate.setTextColor(buttonTextColor);
+    }
+
+    private void initListeners() {
+        mBtnUpdate.setOnClickListener(this);
+        mBtnBackgroundUpdate.setOnClickListener(this);
+        mIvClose.setOnClickListener(this);
+        mTvIgnore.setOnClickListener(this);
+    }
+
+    @Override
+    public void onClick(View view) {
+        int i = view.getId();
+        // 点击版本升级按钮【下载apk】
+        if (i == R.id.btn_update) {
+            // 权限判断是否有访问外部存储空间权限
+            int flag = ActivityCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
+            if (!UpdateUtils.isPrivateApkCacheDir(mUpdateEntity) && flag != PackageManager.PERMISSION_GRANTED) {
+                requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_CODE_REQUEST_PERMISSIONS);
+            } else {
+                installApp();
+            }
+        } else if (i == R.id.btn_background_update) {
+            // 点击后台更新按钮
+            if (sIPrompterProxy != null) {
+                sIPrompterProxy.backgroundDownload();
+            }
+            dismissDialog();
+        } else if (i == R.id.iv_close) {
+            // 点击关闭按钮
+            if (sIPrompterProxy != null) {
+                sIPrompterProxy.cancelDownload();
+            }
+            dismissDialog();
+        } else if (i == R.id.tv_ignore) {
+            // 点击忽略按钮
+            UpdateUtils.saveIgnoreVersion(getActivity(), mUpdateEntity.getVersionName());
+            dismissDialog();
+        }
+    }
+
+    @Override
+    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+        if (requestCode == REQUEST_CODE_REQUEST_PERMISSIONS) {
+            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                // 升级
+                installApp();
+            } else {
+                _XUpdate.onUpdateError(DOWNLOAD_PERMISSION_DENIED);
+                dismissDialog();
+            }
+        }
+
+    }
+
+    private void installApp() {
+        if (UpdateUtils.isApkDownloaded(mUpdateEntity)) {
+            onInstallApk();
+            // 安装完自杀
+            // 如果上次是强制更新,但是用户在下载完,强制杀掉后台,重新启动app后,则会走到这一步,所以要进行强制更新的判断。
+            if (!mUpdateEntity.isForce()) {
+                dismissDialog();
+            } else {
+                showInstallButton();
+            }
+        } else {
+            if (sIPrompterProxy != null) {
+                sIPrompterProxy.startDownload(mUpdateEntity, new WeakFileDownloadListener(this));
+            }
+            // 忽略版本在点击更新按钮后隐藏
+            if (mUpdateEntity.isIgnorable()) {
+                mTvIgnore.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    @Override
+    public void handleStart() {
+        if (!UpdateDialogFragment.this.isRemoving()) {
+            doStart();
+        }
+    }
+
+    private void doStart() {
+        mNumberProgressBar.setVisibility(View.VISIBLE);
+        mNumberProgressBar.setProgress(0);
+        mBtnUpdate.setVisibility(View.GONE);
+        if (mPromptEntity.isSupportBackgroundUpdate()) {
+            mBtnBackgroundUpdate.setVisibility(View.VISIBLE);
+        } else {
+            mBtnBackgroundUpdate.setVisibility(View.GONE);
+        }
+    }
+
+    @Override
+    public void handleProgress(float progress) {
+        if (!UpdateDialogFragment.this.isRemoving()) {
+            if (mNumberProgressBar.getVisibility() == View.GONE) {
+                doStart();
+            }
+            mNumberProgressBar.setProgress(Math.round(progress * 100));
+            mNumberProgressBar.setMax(100);
+        }
+    }
+
+    @Override
+    public boolean handleCompleted(File file) {
+        if (!UpdateDialogFragment.this.isRemoving()) {
+            mBtnBackgroundUpdate.setVisibility(View.GONE);
+            if (mUpdateEntity.isForce()) {
+                showInstallButton();
+            } else {
+                dismissDialog();
+            }
+        }
+        // 返回true,自动进行apk安装
+        return true;
+    }
+
+    @Override
+    public void handleError(Throwable throwable) {
+        if (!UpdateDialogFragment.this.isRemoving()) {
+            if (mPromptEntity.isIgnoreDownloadError()) {
+                refreshUpdateButton();
+            } else {
+                dismissDialog();
+            }
+        }
+    }
+
+    /**
+     * 刷新升级按钮显示
+     */
+    private void refreshUpdateButton() {
+        if (UpdateUtils.isApkDownloaded(mUpdateEntity)) {
+            showInstallButton();
+        } else {
+            showUpdateButton();
+        }
+//        mTvIgnore.setVisibility(mUpdateEntity.isIgnorable() ? View.VISIBLE : View.GONE);
+    }
+
+    /**
+     * 显示安装的按钮
+     */
+    private void showInstallButton() {
+        mNumberProgressBar.setVisibility(View.GONE);
+        mBtnBackgroundUpdate.setVisibility(View.GONE);
+        mBtnUpdate.setText(R.string.xupdate_lab_install);
+        mBtnUpdate.setVisibility(View.VISIBLE);
+        mBtnUpdate.setOnClickListener(this);
+    }
+
+    /**
+     * 显示升级的按钮
+     */
+    private void showUpdateButton() {
+        mNumberProgressBar.setVisibility(View.GONE);
+        mBtnBackgroundUpdate.setVisibility(View.GONE);
+        mBtnUpdate.setText(R.string.xupdate_lab_update);
+        mBtnUpdate.setVisibility(View.VISIBLE);
+        mBtnUpdate.setOnClickListener(this);
+    }
+
+    private void onInstallApk() {
+        _XUpdate.startInstallApk(getContext(), UpdateUtils.getApkFileByUpdateEntity(mUpdateEntity), mUpdateEntity.getDownLoadEntity());
+    }
+
+    /**
+     * 弹窗消失
+     */
+    private void dismissDialog() {
+        _XUpdate.setIsPrompterShow(getUrl(), false);
+        clearIPrompterProxy();
+        dismissAllowingStateLoss();
+    }
+
+    @Override
+    public void show(@NonNull FragmentManager manager, @Nullable String tag) {
+        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
+            if (manager.isDestroyed() || manager.isStateSaved()) {
+                return;
+            }
+        }
+        try {
+            super.show(manager, tag);
+        } catch (Exception e) {
+            _XUpdate.onUpdateError(PROMPT_UNKNOWN, e.getMessage());
+        }
+    }
+
+    /**
+     * 显示更新提示
+     *
+     * @param manager 管理者
+     */
+    public void show(FragmentManager manager) {
+        show(manager, "update_dialog");
+    }
+
+    @Override
+    public void onDestroyView() {
+        _XUpdate.setIsPrompterShow(getUrl(), false);
+        clearIPrompterProxy();
+        super.onDestroyView();
+    }
+
+    private static void setIPrompterProxy(IPrompterProxy prompterProxy) {
+        UpdateDialogFragment.sIPrompterProxy = prompterProxy;
+    }
+
+    private static void clearIPrompterProxy() {
+        if (sIPrompterProxy != null) {
+            sIPrompterProxy.recycle();
+            sIPrompterProxy = null;
+        }
+    }
+
+    @Override
+    public void onConfigurationChanged(@NonNull Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        if (newConfig.orientation != mCurrentOrientation) {
+            reloadView();
+        }
+        mCurrentOrientation = newConfig.orientation;
+    }
+
+    private void reloadView() {
+        View view = LayoutInflater.from(getContext()).inflate(R.layout.xupdate_layout_update_prompter, null);
+        ViewGroup root = (ViewGroup) getView();
+        if (root != null) {
+            root.removeAllViews();
+            root.addView(view);
+            initView(root);
+            initData();
+        }
+    }
+
+    private String getUrl() {
+        return sIPrompterProxy != null ? sIPrompterProxy.getUrl() : "";
+    }
+
+}
+

+ 75 - 0
xupdate-lib/src/main/java/com/xuexiang/xupdate/widget/WeakFileDownloadListener.java

@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2020 xuexiangjys(xuexiangjys@163.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.xuexiang.xupdate.widget;
+
+import com.xuexiang.xupdate.service.OnFileDownloadListener;
+
+import java.io.File;
+import java.lang.ref.WeakReference;
+
+import androidx.annotation.NonNull;
+
+/**
+ * 弱引用文件下载监听, 解决内存泄漏问题
+ *
+ * @author xuexiang
+ * @since 2020/11/15 10:58 PM
+ */
+public class WeakFileDownloadListener implements OnFileDownloadListener {
+
+    private WeakReference<IDownloadEventHandler> mDownloadHandlerRef;
+
+    public WeakFileDownloadListener(@NonNull IDownloadEventHandler handler) {
+        mDownloadHandlerRef = new WeakReference<>(handler);
+    }
+
+    @Override
+    public void onStart() {
+        if (getEventHandler() != null) {
+            getEventHandler().handleStart();
+        }
+    }
+
+    @Override
+    public void onProgress(float progress, long total) {
+        if (getEventHandler() != null) {
+            getEventHandler().handleProgress(progress);
+        }
+    }
+
+    @Override
+    public boolean onCompleted(File file) {
+        if (getEventHandler() != null) {
+            return getEventHandler().handleCompleted(file);
+        } else {
+            // 下载好了,返回true,自动进行apk安装
+            return true;
+        }
+    }
+
+    @Override
+    public void onError(Throwable throwable) {
+        if (getEventHandler() != null) {
+            getEventHandler().handleError(throwable);
+        }
+    }
+
+    private IDownloadEventHandler getEventHandler() {
+        return mDownloadHandlerRef != null ? mDownloadHandlerRef.get() : null;
+    }
+}

+ 8 - 0
xupdate-lib/src/main/res/anim/xupdate_app_window_in.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+    <alpha
+        android:duration="250"
+        android:fromAlpha="0.0"
+        android:toAlpha="1.0"/>
+</set>    
+

+ 9 - 0
xupdate-lib/src/main/res/anim/xupdate_app_window_out.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <alpha
+        android:duration="250"
+        android:fromAlpha="1.0"
+        android:toAlpha="0.0"/>
+</set>   
+

二進制
xupdate-lib/src/main/res/drawable-hdpi/xupdate_bg_app_top.png


二進制
xupdate-lib/src/main/res/drawable-v17/xupdate_icon_app_update.png


File diff suppressed because it is too large
+ 26 - 0
xupdate-lib/src/main/res/drawable-v21/xupdate_icon_app_update.xml


+ 23 - 0
xupdate-lib/src/main/res/drawable/xupdate_bg_app_info.xml

@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <corners
+        android:bottomLeftRadius="@dimen/xupdate_bg_app_info_radius"
+        android:bottomRightRadius="@dimen/xupdate_bg_app_info_radius" />
+    <solid android:color="@color/xupdate_default_bg_color" />
+</shape>

File diff suppressed because it is too large
+ 9 - 0
xupdate-lib/src/main/res/drawable/xupdate_icon_app_close.xml


File diff suppressed because it is too large
+ 26 - 0
xupdate-lib/src/main/res/drawable/xupdate_icon_app_update.xml


+ 143 - 0
xupdate-lib/src/main/res/layout-land/xupdate_layout_update_prompter.xml

@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:orientation="vertical">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_top"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:scaleType="fitXY"
+            tools:srcCompat="@drawable/xupdate_bg_app_top" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:background="@drawable/xupdate_bg_app_info"
+            android:orientation="vertical"
+            android:padding="@dimen/xupdate_content_padding">
+
+            <TextView
+                android:id="@+id/tv_title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:textColor="@color/xupdate_title_text_color"
+                android:textSize="@dimen/xupdate_title_text_size"
+                tools:text="是否升级到4.1.1版本?" />
+
+            <androidx.core.widget.NestedScrollView
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_weight="1"
+                android:overScrollMode="never"
+                android:paddingTop="@dimen/xupdate_common_padding"
+                android:paddingBottom="@dimen/xupdate_common_padding">
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    android:layout_weight="1"
+                    android:orientation="vertical">
+
+                    <TextView
+                        android:id="@+id/tv_update_info"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:lineSpacingExtra="@dimen/xupdate_content_line_spacing_extra_size"
+                        android:textColor="@color/xupdate_content_text_color"
+                        android:textSize="@dimen/xupdate_content_text_size"
+                        tools:text="1、xxxxxxxx\n2、ooooooooo" />
+                </LinearLayout>
+
+            </androidx.core.widget.NestedScrollView>
+
+            <Button
+                android:id="@+id/btn_update"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:minHeight="@dimen/xupdate_button_min_height"
+                android:text="@string/xupdate_lab_update"
+                android:textColor="@color/xupdate_button_text_color"
+                android:textSize="@dimen/xupdate_button_text_size" />
+
+            <TextView
+                android:id="@+id/tv_ignore"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:minHeight="@dimen/xupdate_button_min_height"
+                android:text="@string/xupdate_lab_ignore"
+                android:textColor="@color/xupdate_content_text_color"
+                android:textSize="@dimen/xupdate_button_text_size"
+                android:visibility="gone" />
+
+            <com.xuexiang.xupdate.widget.NumberProgressBar
+                android:id="@+id/npb_progress"
+                style="@style/XUpdate_ProgressBar_Red"
+                android:paddingTop="@dimen/xupdate_common_padding"
+                android:paddingBottom="@dimen/xupdate_common_padding"
+                android:visibility="gone" />
+
+            <Button
+                android:id="@+id/btn_background_update"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:minHeight="@dimen/xupdate_button_min_height_mini"
+                android:text="@string/xupdate_lab_background_update"
+                android:textColor="@color/xupdate_button_text_color"
+                android:textSize="@dimen/xupdate_button_text_size_mini"
+                android:visibility="gone" />
+
+        </LinearLayout>
+
+
+        <LinearLayout
+            android:id="@+id/ll_close"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center"
+            android:gravity="center"
+            android:orientation="vertical">
+
+            <View
+                android:layout_width="@dimen/xupdate_close_line_width"
+                android:layout_height="@dimen/xupdate_close_line_height"
+                android:background="@color/xupdate_close_line_color" />
+
+            <androidx.appcompat.widget.AppCompatImageView
+                android:id="@+id/iv_close"
+                android:layout_width="@dimen/xupdate_close_icon_size"
+                android:layout_height="@dimen/xupdate_close_icon_size"
+                android:layout_marginTop="-2dp"
+                app:srcCompat="@drawable/xupdate_icon_app_close" />
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+
+</FrameLayout>

+ 144 - 0
xupdate-lib/src/main/res/layout/xupdate_dialog_update.xml

@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:orientation="vertical">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_top"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:scaleType="fitXY"
+            tools:srcCompat="@drawable/xupdate_bg_app_top" />
+
+        <androidx.core.widget.NestedScrollView
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:overScrollMode="never"
+            android:scrollbars="none">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_weight="1"
+                android:background="@drawable/xupdate_bg_app_info"
+                android:orientation="vertical"
+                android:padding="@dimen/xupdate_content_padding">
+
+                <TextView
+                    android:id="@+id/tv_title"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:textColor="@color/xupdate_title_text_color"
+                    android:textSize="@dimen/xupdate_title_text_size"
+                    tools:text="是否升级到4.1.1版本?" />
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    android:layout_weight="1"
+                    android:orientation="vertical"
+                    android:paddingTop="@dimen/xupdate_common_padding"
+                    android:paddingBottom="@dimen/xupdate_common_padding">
+
+                    <TextView
+                        android:id="@+id/tv_update_info"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:lineSpacingExtra="@dimen/xupdate_content_line_spacing_extra_size"
+                        android:textColor="@color/xupdate_content_text_color"
+                        android:textSize="@dimen/xupdate_content_text_size"
+                        tools:text="1、xxxxxxxx\n2、ooooooooo" />
+
+                </LinearLayout>
+
+                <Button
+                    android:id="@+id/btn_update"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:gravity="center"
+                    android:minHeight="@dimen/xupdate_button_min_height"
+                    android:text="@string/xupdate_lab_update"
+                    android:textColor="@color/xupdate_button_text_color"
+                    android:textSize="@dimen/xupdate_button_text_size" />
+
+                <TextView
+                    android:id="@+id/tv_ignore"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:gravity="center"
+                    android:minHeight="@dimen/xupdate_button_min_height"
+                    android:text="@string/xupdate_lab_ignore"
+                    android:textColor="@color/xupdate_content_text_color"
+                    android:textSize="@dimen/xupdate_button_text_size"
+                    android:visibility="gone" />
+
+                <com.xuexiang.xupdate.widget.NumberProgressBar
+                    android:id="@+id/npb_progress"
+                    style="@style/XUpdate_ProgressBar_Red"
+                    android:paddingTop="@dimen/xupdate_common_padding"
+                    android:paddingBottom="@dimen/xupdate_common_padding"
+                    android:visibility="gone" />
+
+                <Button
+                    android:id="@+id/btn_background_update"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:gravity="center"
+                    android:minHeight="@dimen/xupdate_button_min_height_mini"
+                    android:text="@string/xupdate_lab_background_update"
+                    android:textColor="@color/xupdate_button_text_color"
+                    android:textSize="@dimen/xupdate_button_text_size_mini"
+                    android:visibility="gone" />
+
+            </LinearLayout>
+        </androidx.core.widget.NestedScrollView>
+
+
+        <LinearLayout
+            android:id="@+id/ll_close"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:gravity="center_horizontal"
+            android:visibility="gone"
+            android:orientation="vertical">
+
+            <View
+                android:layout_width="@dimen/xupdate_close_line_width"
+                android:layout_height="@dimen/xupdate_close_line_height"
+                android:background="@color/xupdate_close_line_color" />
+
+            <androidx.appcompat.widget.AppCompatImageView
+                android:id="@+id/iv_close"
+                android:layout_width="@dimen/xupdate_close_icon_size"
+                android:layout_height="@dimen/xupdate_close_icon_size"
+                android:layout_marginTop="-2dp"
+                app:srcCompat="@drawable/xupdate_icon_app_close" />
+
+        </LinearLayout>
+    </LinearLayout>
+
+
+</FrameLayout>

+ 144 - 0
xupdate-lib/src/main/res/layout/xupdate_layout_update_prompter.xml

@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:orientation="vertical">
+
+        <androidx.appcompat.widget.AppCompatImageView
+            android:id="@+id/iv_top"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:scaleType="fitXY"
+            tools:srcCompat="@drawable/xupdate_bg_app_top" />
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:background="@drawable/xupdate_bg_app_info"
+            android:orientation="vertical"
+            android:padding="@dimen/xupdate_content_padding">
+
+            <TextView
+                android:id="@+id/tv_title"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:textColor="@color/xupdate_title_text_color"
+                android:textSize="@dimen/xupdate_title_text_size"
+                tools:text="是否升级到4.1.1版本?" />
+
+            <androidx.core.widget.NestedScrollView
+                android:layout_width="match_parent"
+                android:layout_height="0dp"
+                android:layout_weight="1"
+                android:overScrollMode="never"
+                android:paddingTop="@dimen/xupdate_common_padding"
+                android:paddingBottom="@dimen/xupdate_common_padding">
+
+                <LinearLayout
+                    android:layout_width="match_parent"
+                    android:layout_height="0dp"
+                    android:layout_weight="1"
+                    android:orientation="vertical">
+
+                    <TextView
+                        android:id="@+id/tv_update_info"
+                        android:layout_width="match_parent"
+                        android:layout_height="wrap_content"
+                        android:lineSpacingExtra="@dimen/xupdate_content_line_spacing_extra_size"
+                        android:textColor="@color/xupdate_content_text_color"
+                        android:textSize="@dimen/xupdate_content_text_size"
+                        tools:text="1、xxxxxxxx\n2、ooooooooo" />
+
+                </LinearLayout>
+
+            </androidx.core.widget.NestedScrollView>
+
+            <Button
+                android:id="@+id/btn_update"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:minHeight="@dimen/xupdate_button_min_height"
+                android:text="@string/xupdate_lab_update"
+                android:textColor="@color/xupdate_button_text_color"
+                android:textSize="@dimen/xupdate_button_text_size" />
+
+            <TextView
+                android:id="@+id/tv_ignore"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:minHeight="@dimen/xupdate_button_min_height"
+                android:text="@string/xupdate_lab_ignore"
+                android:textColor="@color/xupdate_content_text_color"
+                android:textSize="@dimen/xupdate_button_text_size"
+                android:visibility="gone" />
+
+            <com.xuexiang.xupdate.widget.NumberProgressBar
+                android:id="@+id/npb_progress"
+                style="@style/XUpdate_ProgressBar_Red"
+                android:paddingTop="@dimen/xupdate_common_padding"
+                android:paddingBottom="@dimen/xupdate_common_padding"
+                android:visibility="gone" />
+
+            <Button
+                android:id="@+id/btn_background_update"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:minHeight="@dimen/xupdate_button_min_height_mini"
+                android:text="@string/xupdate_lab_background_update"
+                android:textColor="@color/xupdate_button_text_color"
+                android:textSize="@dimen/xupdate_button_text_size_mini"
+                android:visibility="gone" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/ll_close"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_horizontal"
+            android:gravity="center_horizontal"
+            android:orientation="vertical">
+
+            <View
+                android:layout_width="@dimen/xupdate_close_line_width"
+                android:layout_height="@dimen/xupdate_close_line_height"
+                android:background="@color/xupdate_close_line_color" />
+
+            <androidx.appcompat.widget.AppCompatImageView
+                android:id="@+id/iv_close"
+                android:layout_width="@dimen/xupdate_close_icon_size"
+                android:layout_height="@dimen/xupdate_close_icon_size"
+                android:layout_marginTop="-2dp"
+                app:srcCompat="@drawable/xupdate_icon_app_close" />
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+</FrameLayout>

+ 49 - 0
xupdate-lib/src/main/res/values-en-rUS/xupdate_strings.xml

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <!--  Update the content used in the interface, which needs to be configured for the internationalization of the interface  -->
+    <string name="xupdate_lab_update">Upgrade</string>
+    <string name="xupdate_lab_background_update">Background Upgrade</string>
+    <string name="xupdate_lab_install">Install</string>
+    <string name="xupdate_lab_ignore">Ignore</string>
+    <string name="xupdate_tip_permissions_reject">Please authorize access to storage space permissions, otherwise App can not be updated!</string>
+    <string name="xupdate_connecting_service">Connecting the server…</string>
+    <string name="xupdate_start_download">Start download</string>
+    <string name="xupdate_download_complete">Download to complete, please click Install</string>
+    <string name="xupdate_lab_ready_update">Whether to upgrade to version %s?</string>
+    <string name="xupdate_lab_new_version_size">New version size:</string>
+    <string name="xupdate_lab_downloading">Downloading:</string>
+    <string name="xupdate_tip_download_url_error">New version download path error</string>
+
+    <!--  Update error message  -->
+    <string name="xupdate_error_install_failed">Failed to install APK!</string>
+    <string name="xupdate_error_download_permission_denied">Unable to download: storage permission request rejected!</string>
+    <string name="xupdate_error_download_failed">Download failed!</string>
+    <string name="xupdate_error_prompt_activity_destroy">Failed: the activity has been destroyed!</string>
+    <string name="xupdate_error_prompt_unknown">Failed: unknown error!</string>
+    <string name="xupdate_error_check_apk_cache_dir_empty">Update failed: apk download cache directory is empty!</string>
+    <string name="xupdate_error_check_ignored_version">Update failed: ignored version!</string>
+    <string name="xupdate_error_check_parse">Query failure: Json parsing error!</string>
+    <string name="xupdate_error_check_json_empty">Query failed: Json is empty!</string>
+    <string name="xupdate_error_check_no_new_version">It\'s the latest version!</string>
+    <string name="xupdate_error_check_updating">The program is being updated!</string>
+    <string name="xupdate_error_check_no_network">Query failed: no network!</string>
+    <string name="xupdate_error_check_no_wifi">Query failed: no WIFI!</string>
+    <string name="xupdate_error_check_net_request">Query failed: network request error!</string>
+
+</resources>

+ 49 - 0
xupdate-lib/src/main/res/values-zh-rCN/xupdate_strings.xml

@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <!--  更新界面使用到的内容,界面国际化需要配置这些内容  -->
+    <string name="xupdate_lab_update">升级</string>
+    <string name="xupdate_lab_background_update">后台更新</string>
+    <string name="xupdate_lab_install">安装</string>
+    <string name="xupdate_lab_ignore">忽略此版本</string>
+    <string name="xupdate_tip_permissions_reject">请授权访问存储空间权限,否则App无法更新!</string>
+    <string name="xupdate_connecting_service">正在连接服务器…</string>
+    <string name="xupdate_start_download">开始下载</string>
+    <string name="xupdate_download_complete">下载完成,请点击安装</string>
+    <string name="xupdate_lab_ready_update">是否升级到%s版本?</string>
+    <string name="xupdate_lab_new_version_size">新版本大小:</string>
+    <string name="xupdate_lab_downloading">正在下载:</string>
+    <string name="xupdate_tip_download_url_error">新版本下载路径错误</string>
+
+    <!--  更新错误信息  -->
+    <string name="xupdate_error_install_failed">安装APK失败!</string>
+    <string name="xupdate_error_download_permission_denied">无法下载:存储权限申请被拒绝!</string>
+    <string name="xupdate_error_download_failed">下载失败!</string>
+    <string name="xupdate_error_prompt_activity_destroy">提示失败:activity已被销毁!</string>
+    <string name="xupdate_error_prompt_unknown">提示失败:未知错误!</string>
+    <string name="xupdate_error_check_apk_cache_dir_empty">更新失败:apk的下载缓存目录为空!</string>
+    <string name="xupdate_error_check_ignored_version">更新失败:已经被忽略的版本!</string>
+    <string name="xupdate_error_check_parse">查询失败:</string>
+    <string name="xupdate_error_check_json_empty">查询失败:Json为空!</string>
+    <string name="xupdate_error_check_no_new_version">已是最新版本!</string>
+    <string name="xupdate_error_check_updating">程序正在进行版本更新!</string>
+    <string name="xupdate_error_check_no_network">网络异常!</string>
+    <string name="xupdate_error_check_no_wifi">查询失败:没有WIFI!</string>
+    <string name="xupdate_error_check_net_request">查询失败:网络请求错误!</string>
+
+</resources>

+ 40 - 0
xupdate-lib/src/main/res/values/xupdate_attrs.xml

@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <declare-styleable name="XNumberProgressBar">
+        <attr name="xnpb_current" format="integer" />
+        <attr name="xnpb_max" format="integer" />
+
+        <attr name="xnpb_unreached_color" format="color" />
+        <attr name="xnpb_reached_color" format="color" />
+
+        <attr name="xnpb_reached_bar_height" format="dimension" />
+        <attr name="xnpb_unreached_bar_height" format="dimension" />
+
+        <attr name="xnpb_text_size" format="dimension" />
+        <attr name="xnpb_text_color" format="color" />
+
+        <attr name="xnpb_text_offset" format="dimension" />
+
+        <attr name="xnpb_text_visibility" format="enum">
+            <enum name="visible" value="0" />
+            <enum name="invisible" value="1" />
+        </attr>
+    </declare-styleable>
+
+
+</resources>

+ 27 - 0
xupdate-lib/src/main/res/values/xupdate_colors.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+
+    <color name="xupdate_default_theme_color">#FF2388FF</color>
+    <color name="xupdate_default_bg_color">#FFFFFFFF</color>
+    <color name="xupdate_close_line_color">#D8D8D8</color>
+    <color name="xupdate_button_text_color">#FFFFFFFF</color>
+    <color name="xupdate_title_text_color">#FF000000</color>
+    <color name="xupdate_content_text_color">#FF666666</color>
+
+</resources>

+ 19 - 0
xupdate-lib/src/main/res/values/xupdate_dimens.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <dimen name="xupdate_bg_app_info_radius">3dp</dimen>
+    <dimen name="xupdate_close_icon_size">30dp</dimen>
+    <dimen name="xupdate_close_line_height">50dp</dimen>
+    <dimen name="xupdate_close_line_width">1dp</dimen>
+    <dimen name="xupdate_common_padding">10dp</dimen>
+    <dimen name="xupdate_content_padding">16dp</dimen>
+    <dimen name="xupdate_title_text_size">15sp</dimen>
+    <dimen name="xupdate_content_text_size">14sp</dimen>
+    <dimen name="xupdate_button_min_height">40dp</dimen>
+    <dimen name="xupdate_button_text_size">15sp</dimen>
+    <dimen name="xupdate_content_line_spacing_extra_size">5dp</dimen>
+    <dimen name="xupdate_button_min_height_mini">35dp</dimen>
+    <dimen name="xupdate_button_text_size_mini">13sp</dimen>
+
+
+</resources>

+ 48 - 0
xupdate-lib/src/main/res/values/xupdate_strings.xml

@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <!--  Update the content used in the interface, which needs to be configured for the internationalization of the interface  -->
+    <string name="xupdate_lab_update">Upgrade</string>
+    <string name="xupdate_lab_background_update">Background Upgrade</string>
+    <string name="xupdate_lab_install">Install</string>
+    <string name="xupdate_lab_ignore">Ignore</string>
+    <string name="xupdate_tip_permissions_reject">Please authorize access to storage space permissions, otherwise App can not be updated!</string>
+    <string name="xupdate_connecting_service">Connecting the server…</string>
+    <string name="xupdate_start_download">Start download</string>
+    <string name="xupdate_download_complete">Download to complete, please click Install</string>
+    <string name="xupdate_lab_ready_update">Whether to upgrade to version %s?</string>
+    <string name="xupdate_lab_new_version_size">New version size:</string>
+    <string name="xupdate_lab_downloading">Downloading:</string>
+    <string name="xupdate_tip_download_url_error">New version download path error</string>
+
+    <!--  Update error message  -->
+    <string name="xupdate_error_install_failed">Failed to install APK!</string>
+    <string name="xupdate_error_download_permission_denied">Unable to download: storage permission request rejected!</string>
+    <string name="xupdate_error_download_failed">Download failed!</string>
+    <string name="xupdate_error_prompt_activity_destroy">Failed: the activity has been destroyed!</string>
+    <string name="xupdate_error_prompt_unknown">Failed: unknown error!</string>
+    <string name="xupdate_error_check_apk_cache_dir_empty">Update failed: apk download cache directory is empty!</string>
+    <string name="xupdate_error_check_ignored_version">Update failed: ignored version!</string>
+    <string name="xupdate_error_check_parse">Query failure: Json parsing error!</string>
+    <string name="xupdate_error_check_json_empty">Query failed: Json is empty!</string>
+    <string name="xupdate_error_check_no_new_version">It\'s the latest version!</string>
+    <string name="xupdate_error_check_updating">The program is being updated!</string>
+    <string name="xupdate_error_check_no_network">Query failed: no network!</string>
+    <string name="xupdate_error_check_no_wifi">Query failed: no WIFI!</string>
+    <string name="xupdate_error_check_net_request">Query failed: network request error!</string>
+
+</resources>

+ 80 - 0
xupdate-lib/src/main/res/values/xupdate_style_widget.xml

@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+
+    <style name="XUpdate_ProgressBar_Red">
+        <item name="android:layout_height">wrap_content</item>
+        <item name="android:layout_width">match_parent</item>
+        <item name="xnpb_max">100</item>
+        <item name="xnpb_current">0</item>
+        <item name="xnpb_unreached_color">#CCCCCC</item>
+        <item name="xnpb_reached_color">#e94339</item>
+        <item name="xnpb_text_size">12sp</item>
+        <item name="xnpb_text_color">#e94339</item>
+        <item name="xnpb_reached_bar_height">4dp</item>
+        <item name="xnpb_unreached_bar_height">3dp</item>
+    </style>
+
+    <style name="XUpdate_Fragment_Dialog" parent="android:style/Theme.Dialog">
+        <item name="android:background">@android:color/transparent</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowCloseOnTouchOutside">false</item>
+        <item name="android:windowEnterAnimation">@anim/xupdate_app_window_in</item>
+        <item name="android:windowExitAnimation">@anim/xupdate_app_window_out</item>
+    </style>
+
+    <!-- BaseDialog -->
+    <style name="XUpdate_Dialog" parent="@android:style/Theme.Dialog">
+        <!-- Dialog的windowFrame框为无 -->
+        <item name="android:windowFrame">@null</item>
+        <item name="android:windowNoTitle">true</item>
+        <!-- 是否漂现在activity上 -->
+        <item name="android:windowIsFloating">true</item>
+        <!-- 是否半透明 -->
+        <item name="android:windowIsTranslucent">false</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:windowAnimationStyle">@null</item>
+        <item name="android:windowSoftInputMode">adjustPan</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:windowEnterAnimation">@anim/xupdate_app_window_in</item>
+        <item name="android:windowExitAnimation">@anim/xupdate_app_window_out</item>
+    </style>
+
+    <!--DialogTheme,用于将Activity作为Dialog的主题-->
+    <style name="XUpdate_DialogTheme" parent="Theme.AppCompat.Light.NoActionBar">
+        <!--设置dialog的背景,此处为系统给定的透明值-->
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <!--Dialog的windowFrame框为无-->
+        <item name="android:windowFrame">@null</item>
+        <!--无标题-->
+        <item name="android:windowNoTitle">true</item>     
+        <!--是否浮现在activity之上-->
+        <item name="android:windowIsFloating">true</item>
+        <!--是否半透明-->
+        <item name="android:windowIsTranslucent">true</item>
+        <!--是否有覆盖-->
+        <item name="android:windowContentOverlay">@null</item>
+        <!--设置Activity出现方式-->
+        <item name="android:windowAnimationStyle">@android:style/Animation.Dialog</item>
+        <item name="android:windowEnterAnimation">@anim/xupdate_app_window_in</item>
+        <item name="android:windowExitAnimation">@anim/xupdate_app_window_out</item>
+        <!--背景是否模糊显示-->
+        <item name="android:backgroundDimEnabled">true</item>
+    </style>
+
+</resources>

+ 38 - 0
xupdate-lib/src/main/res/xml/update_file_paths.xml

@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2018 xuexiangjys(xuexiangjys@163.com)
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~       http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<paths>
+    <files-path
+        name="update_files_path"
+        path="." />
+
+    <cache-path
+        name="update_cache_path"
+        path="." />
+
+    <external-path
+        name="update_external_path"
+        path="." />
+
+    <external-files-path
+        name="update_external_files_path"
+        path="." />
+
+    <external-cache-path
+        name="update_external_cache_path"
+        path="." />
+</paths>