├── .gitignore ├── README.md ├── appupdate ├── .gitignore ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local.properties ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── hailong │ │ └── appupdate │ │ ├── AppUpdateManager.java │ │ ├── CustomProvider.java │ │ ├── utils │ │ ├── ApkUtil.java │ │ ├── FileUtils.java │ │ ├── ImageUtil.java │ │ ├── ViewUtil.java │ │ └── WeakHandler.java │ │ ├── view │ │ └── recyclerview │ │ │ ├── CommonRecycleViewAdapter.java │ │ │ ├── DividerGridItemDecoration.java │ │ │ ├── DividerItemDecoration.java │ │ │ ├── GridItemSpaceDecoration.java │ │ │ ├── MaxHeightRecyclerView.java │ │ │ └── ViewHolder.java │ │ └── widget │ │ └── UpdateDialog.java │ └── res │ ├── drawable │ ├── appupdate_corner20dp_b9b9b9.xml │ ├── appupdate_corner20dp_color_primary.xml │ ├── appupdate_corner20dp_ffffff.xml │ ├── appupdate_corner5dp_b9b9b9.xml │ ├── appupdate_corner5dp_color_primary.xml │ └── appupdate_progress_bg.xml │ ├── layout │ ├── appupdate_dialogfrag_update.xml │ └── appupdate_listitem_update_content.xml │ ├── mipmap-xhdpi │ └── appupdate_bg_app_top.png │ ├── mipmap-xxhdpi │ └── appupdate_bg_app_top.png │ ├── values-en │ └── strings.xml │ ├── values-zh │ └── strings.xml │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── appupdate_fileprovicer_paths.xml ├── build.gradle ├── example ├── .gitignore ├── build.gradle ├── file │ ├── appupdate_example.apk │ ├── update_dialog.jpg │ ├── update_error.jpg │ └── updating.jpg ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── hailong │ │ └── appupdate │ │ └── exmple │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── hailong │ │ │ └── appupdate │ │ │ └── exmple │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ └── network_security_config.xml │ └── test │ └── java │ └── com │ └── hailong │ └── appupdate │ └── exmple │ └── ExampleUnitTest.java ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── keystore └── keystore_drumbeat.jks └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # APP版本更新 2 | 3 | ## 示例 4 | 5 | ![更新提示(图片加载失败了)](https://raw.githubusercontent.com/ZuoHailong/AppUpdate/master/example/file/update_dialog.jpg) 6 | ![正在更新(图片加载失败了)](https://raw.githubusercontent.com/ZuoHailong/AppUpdate/master/example/file/updating.jpg) 7 | ![更新出错(图片加载失败了)](https://raw.githubusercontent.com/ZuoHailong/AppUpdate/master/example/file/update_error.jpg) 8 | 9 | ## 功能介绍 10 | 11 | - 支持指向apk文件的 url 形式的版本更新 12 | 13 | - 支持指向接口、接口返回二进制文件流的 url 形式的版本更新 14 | 15 | - 支持断点下载 16 | 17 | - 采用 Service + AsyncTask 方式下载 18 | 19 | - 提供界面友好的版本更新提示弹窗,可自定义其主题样式 20 | 21 | - 兼容Android 6.0,更新库自动获取写权限,用户拒绝后可再次请求 22 | 23 | - 兼容Android 7.0,支持FileProvider 24 | 25 | - 兼容Android 8.0,应用安装无障碍 26 | 27 | - 实现国际化(支持中文和英文) 28 | 29 | ## Gradle依赖 30 | 31 | ``` 32 | dependencies { 33 | implementation 'com.github.ZuoHailong:AppUpdate:0.2.6' 34 | } 35 | 36 | ``` 37 | ## 简单使用 38 | ``` 39 | AppUpdateManager.Builder builder = new AppUpdateManager.Builder(MainActivity.this); 40 | builder.apkUrl(String apkUrl) 41 | .updateContent(String[] array) 42 | .updateForce(boolean isForce) 43 | .build(); 44 | 45 | ``` 46 | ## Builder详细用法 47 | 48 | ### 一、功能性设置 49 | 50 | #### 1、设置apk下载链接 51 | ``` 52 | builder.apkUrl(String apkUrl) 53 | ``` 54 | #### 2、设置版本更新内容 55 | ``` 56 | builder.apkUrl(String[] array) 57 | ``` 58 | #### 3、设置是否必须更新 59 | ``` 60 | builder.updateForce(boolean isForce) 61 | ``` 62 | #### 4、设置新版本的版本名,形如“1.0.2” 63 | ``` 64 | builder.newVerName(String newVerName) 65 | ``` 66 | #### 5、设置更新框标题,默认“发现新版本” 67 | ``` 68 | builder.title(String title) 69 | ``` 70 | #### 6、设置确认按钮文字,默认“立即更新” 71 | ``` 72 | builder.confirmText(String confirmText) 73 | ``` 74 | #### 7、设置取消按钮文字,默认“暂不更新” 75 | ``` 76 | builder.cancelText(String cancelText) 77 | ``` 78 | #### 8、设置待下载apk文件大小 79 | ``` 80 | builder.apkContentLength(long apkContentLength) 81 | ``` 82 | ##### 注意: 83 | * 当apkUrl直接指向待下载文件时,不需要作此设置,可自动获取待下载文件大小; 84 | * 当apkUrl指向server端接口,在接口的response中以二进制流形式下载时,此值必需设置,否则会提示“更新出错”。 85 | 86 | #### 9、设置是否支持断点下载 87 | ``` 88 | builder.breakpoint(boolean breakpoint) 89 | ``` 90 | ##### 注意: 91 | * 当apkUrl直接指向待下载文件时,不需要作此设置,默认支持断点下载; 92 | * 当apkUrl指向server端接口,在接口的response中以二进制流形式下载时,此值是可选设置,默认为false(若设为true,一定要与server端小伙伴确定是否支持断点下载)。 93 | 94 | #### 10、开始构建并弹出更新框 95 | ``` 96 | builder.build() 97 | ``` 98 | 99 | ### 二、样式设置 100 | 101 | #### 1、设置更新框顶部背景图,要求传入drawable资源id 102 | ``` 103 | builder.topResId(@DrawableRes int topResId) 104 | ``` 105 | #### 2、设置确定按钮背景色,要求传入Color资源 106 | ``` 107 | builder.confirmBgColor(@ColorInt int color) 108 | ``` 109 | #### 3、设置确定按钮背景图,要求传入drawable资源id 110 | ``` 111 | builder.confirmBgResource(@DrawableRes int resid) 112 | ``` 113 | #### 4、设置取消按钮背景色,要求传入Color资源 114 | ``` 115 | builder.cancelBgColor(@ColorInt int color) 116 | ``` 117 | #### 5、设置取消按钮背景图,要求传入drawable资源id 118 | ``` 119 | builder.cancelBgResource(@DrawableRes int resid) 120 | ``` 121 | #### 6、设置进度条样式,要求传入drawable资源id 122 | ``` 123 | builder.progressDrawable(@DrawableRes int resid) 124 | ``` 125 | 126 | ### 更多功能待续…… 127 | -------------------------------------------------------------------------------- /appupdate/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /appupdate/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.github.dcendents.android-maven' 3 | group = 'com.github.ZuoHailong' 4 | 5 | android { 6 | compileSdkVersion 28 7 | 8 | defaultConfig { 9 | minSdkVersion 19 10 | targetSdkVersion 28 11 | versionCode 26 12 | versionName "0.2.6" 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | compileOptions { 23 | sourceCompatibility JavaVersion.VERSION_1_8 24 | targetCompatibility JavaVersion.VERSION_1_8 25 | } 26 | 27 | } 28 | 29 | allprojects { 30 | afterEvaluate { 31 | android { 32 | resourcePrefix "${project.name}_" 33 | } 34 | } 35 | } 36 | 37 | dependencies { 38 | implementation fileTree(dir: 'libs', include: ['*.jar']) 39 | implementation 'androidx.recyclerview:recyclerview:1.1.0' 40 | implementation "androidx.constraintlayout:constraintlayout:1.1.3" 41 | 42 | // Kalle 43 | implementation 'com.yanzhenjie:kalle:0.1.7' 44 | } 45 | -------------------------------------------------------------------------------- /appupdate/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/appupdate/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /appupdate/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jul 18 18:48:34 CST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip 7 | -------------------------------------------------------------------------------- /appupdate/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /appupdate/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /appupdate/local.properties: -------------------------------------------------------------------------------- 1 | ## This file must *NOT* be checked into Version Control Systems, 2 | # as it contains information specific to your local configuration. 3 | # 4 | # Location of the SDK. This is only used by Gradle. 5 | # For customization when using a Version Control System, please read the 6 | # header note. 7 | #Thu Jul 18 18:48:29 CST 2019 8 | sdk.dir=C\:\\Users\\Administrator\\AppData\\Local\\Android\\Sdk 9 | -------------------------------------------------------------------------------- /appupdate/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /appupdate/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 12 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | -------------------------------------------------------------------------------- /appupdate/src/main/java/com/hailong/appupdate/AppUpdateManager.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate; 2 | 3 | import android.app.Activity; 4 | 5 | import androidx.annotation.ColorInt; 6 | import androidx.annotation.DrawableRes; 7 | import androidx.annotation.NonNull; 8 | 9 | import com.hailong.appupdate.widget.UpdateDialog; 10 | 11 | /** 12 | * Describe:app版本更新管理器 13 | * Created by ZuoHailong on 2019/5/25. 14 | */ 15 | public class AppUpdateManager { 16 | 17 | private AppUpdateManager(Builder builder) { 18 | UpdateDialog updateDialog = UpdateDialog.newInstance(builder.context); 19 | updateDialog.setUpdateContent(builder.updateContent) 20 | .setUpdateForce(builder.updateForce) 21 | .setNewVernName(builder.newVerName) 22 | .setTopResId(builder.topResId) 23 | .setConfirmBgColor(builder.confirmBgColor) 24 | .setCancelBgColor(builder.cancelBgColor) 25 | .setConfirmBgResource(builder.confirmBgResource) 26 | .setCancelBgResource(builder.cancelBgResource) 27 | .setProgressDrawable(builder.progressDrawable) 28 | .setApkUrl(builder.apkUrl) 29 | .setTitle(builder.title) 30 | .setConfirmText(builder.confirmText) 31 | .setCancelText(builder.cancleText) 32 | // .setApkContentLength(builder.apkContentLength) 33 | // .isBreakpoint(builder.breakpoint) 34 | .show(builder.context.getFragmentManager(), "update"); 35 | } 36 | 37 | /** 38 | * UpdateAppManager的构建器 39 | */ 40 | public static class Builder { 41 | 42 | /*必选字段*/ 43 | private Activity context; 44 | 45 | /*可选字段*/ 46 | private boolean updateForce;//是否强制更新,默认false 47 | private String apkUrl;//apk下载链接 48 | private String[] updateContent;//版本更新内容 49 | private String title; 50 | private String newVerName; 51 | private String confirmText; 52 | private String cancleText; 53 | private int topResId;//标题背景的资源图片 54 | private int confirmBgColor;//确定按钮背景色 55 | private int cancelBgColor;//取消按钮背景色 56 | private int confirmBgResource;//确定按钮背景 57 | private int cancelBgResource;//取消按钮背景 58 | private int progressDrawable;//进度条样式 59 | // private long apkContentLength;//apk文件大小 60 | // private boolean breakpoint;//是否支持断点下载,默认不支持(针对访问接口获取文件流的下载方式) 61 | 62 | /** 63 | * 构建器 64 | * 65 | * @param activity 66 | */ 67 | public Builder(@NonNull Activity activity) { 68 | this.context = activity; 69 | } 70 | 71 | /** 72 | * 设置apk下载链接 73 | * 74 | * @param apkUrl 75 | * @return 76 | */ 77 | public Builder apkUrl(String apkUrl) { 78 | this.apkUrl = apkUrl; 79 | return this; 80 | } 81 | 82 | /** 83 | * 设置版本更新内容 84 | * 85 | * @param updateContent 86 | * @return 87 | */ 88 | public Builder updateContent(String[] updateContent) { 89 | this.updateContent = updateContent; 90 | return this; 91 | } 92 | 93 | /** 94 | * 是否必须更新 95 | * 96 | * @param isForce 97 | * @return 98 | */ 99 | public Builder updateForce(boolean isForce) { 100 | this.updateForce = isForce; 101 | return this; 102 | } 103 | 104 | /** 105 | * 设置新版本号 106 | * 107 | * @param newVerName 108 | * @return 109 | */ 110 | public Builder newVerName(String newVerName) { 111 | this.newVerName = newVerName; 112 | return this; 113 | } 114 | 115 | /** 116 | * 更新框标题 117 | * 118 | * @param title 119 | * @return 120 | */ 121 | public Builder title(String title) { 122 | this.title = title; 123 | return this; 124 | } 125 | 126 | /** 127 | * 设置确认按钮文字 128 | * 129 | * @param confirmText 130 | * @return 131 | */ 132 | public Builder confirmText(String confirmText) { 133 | this.confirmText = confirmText; 134 | return this; 135 | } 136 | 137 | /** 138 | * 设置取消按钮文字 139 | * 140 | * @param cancelText 141 | * @return 142 | */ 143 | public Builder cancelText(String cancelText) { 144 | this.cancleText = cancelText; 145 | return this; 146 | } 147 | 148 | /** 149 | * apk文件大小 150 | * 151 | * @param apkContentLength 152 | * @return 153 | */ 154 | /*public Builder apkContentLength(long apkContentLength) { 155 | this.apkContentLength = apkContentLength; 156 | return this; 157 | }*/ 158 | 159 | /** 160 | * 是否支持断点下载,默认不支持 161 | * 注意:采用直接指向文件的链接方式下载,不需要作此设置,默认支持断点下载; 162 | * 当采用访问接口获取二进制流的方式下载时,需要向server端开发人员核实是否支持断点下载。 163 | * 164 | * @param breakpoint 165 | * @return 166 | */ 167 | /*public Builder breakpoint(boolean breakpoint) { 168 | this.breakpoint = breakpoint; 169 | return this; 170 | }*/ 171 | 172 | /** 173 | * 设置标题背景的资源图片 174 | * 175 | * @param topResId 176 | * @return 177 | */ 178 | public Builder topResId(@DrawableRes int topResId) { 179 | this.topResId = topResId; 180 | return this; 181 | } 182 | 183 | /** 184 | * 设置确定按钮背景色 185 | * 186 | * @param color 187 | * @return 188 | */ 189 | public Builder confirmBgColor(@ColorInt int color) { 190 | this.confirmBgColor = color; 191 | return this; 192 | } 193 | 194 | /** 195 | * 设置确定按钮背景 196 | * 197 | * @param resid 198 | * @return 199 | */ 200 | public Builder confirmBgResource(@DrawableRes int resid) { 201 | this.confirmBgResource = resid; 202 | return this; 203 | } 204 | 205 | /** 206 | * 设置取消按钮背景色 207 | * 208 | * @param color 209 | * @return 210 | */ 211 | public Builder cancelBgColor(@ColorInt int color) { 212 | this.cancelBgColor = color; 213 | return this; 214 | } 215 | 216 | /** 217 | * 设置取消按钮背景 218 | * 219 | * @param resid 220 | * @return 221 | */ 222 | public Builder cancelBgResource(@DrawableRes int resid) { 223 | this.cancelBgResource = resid; 224 | return this; 225 | } 226 | 227 | /** 228 | * 设置进度条样式 229 | * 230 | * @param resid 231 | * @return 232 | */ 233 | public Builder progressDrawable(@DrawableRes int resid) { 234 | this.progressDrawable = resid; 235 | return this; 236 | } 237 | 238 | /** 239 | * 开始构建 240 | * 241 | * @return 242 | */ 243 | public AppUpdateManager build() { 244 | return new AppUpdateManager(this); 245 | } 246 | } 247 | 248 | } 249 | -------------------------------------------------------------------------------- /appupdate/src/main/java/com/hailong/appupdate/CustomProvider.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate; 2 | 3 | import androidx.core.content.FileProvider; 4 | 5 | /** 6 | * FileProvider配置访问路径,适配7.0及其以上 7 | * Created by ZuoHailong on 2019/9/18. 8 | */ 9 | public class CustomProvider extends FileProvider { 10 | } 11 | -------------------------------------------------------------------------------- /appupdate/src/main/java/com/hailong/appupdate/utils/ApkUtil.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate.utils; 2 | 3 | import android.app.Activity; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.pm.PackageInfo; 8 | import android.content.pm.PackageManager; 9 | import android.content.pm.ResolveInfo; 10 | import android.net.Uri; 11 | import android.os.Build; 12 | import android.text.TextUtils; 13 | 14 | import androidx.core.content.FileProvider; 15 | 16 | import java.io.BufferedReader; 17 | import java.io.File; 18 | import java.io.IOException; 19 | import java.io.InputStreamReader; 20 | import java.util.List; 21 | 22 | /** 23 | * Created by ZuoHailong on 2019/7/5. 24 | */ 25 | public class ApkUtil { 26 | 27 | /** 28 | * 判断 APP 是否安装 29 | * 30 | * @param context 活动对应的上下文对象 31 | * @param packageName 需要检查的应用包名 32 | * @return 33 | */ 34 | public static boolean isInstalled(Context context, String packageName) { 35 | try { 36 | Process process = Runtime.getRuntime().exec("pm list package -3"); 37 | BufferedReader bis = new BufferedReader(new InputStreamReader(process.getInputStream())); 38 | String line = null; 39 | while ((line = bis.readLine()) != null) { 40 | System.out.println("MainActivity.runCommand, line=" + line.substring(8, line.length())); 41 | if (packageName.equals(line.substring(8, line.length()))) { 42 | return true; 43 | } 44 | } 45 | } catch (IOException e) { 46 | System.out.println("MainActivity.runCommand,e=" + e); 47 | } 48 | return false; 49 | } 50 | 51 | /** 52 | * 打开某一应用 53 | * 54 | * @param context 活动对应上下文对象 55 | * @param packagename 需要打开的应用包名 56 | */ 57 | public static void openApp(Activity context, String packagename) { 58 | 59 | // 通过包名获取此APP详细信息,包括Activities、services、versioncode、name等等 60 | PackageInfo packageinfo = null; 61 | try { 62 | packageinfo = context.getPackageManager().getPackageInfo(packagename, 0); 63 | } catch (PackageManager.NameNotFoundException e) { 64 | e.printStackTrace(); 65 | } 66 | if (packageinfo == null) { 67 | return; 68 | } 69 | 70 | // 创建一个类别为CATEGORY_LAUNCHER的该包名的Intent 71 | Intent resolveIntent = new Intent(Intent.ACTION_MAIN, null); 72 | resolveIntent.addCategory(Intent.CATEGORY_LAUNCHER); 73 | resolveIntent.setPackage(packageinfo.packageName); 74 | 75 | // 通过getPackageManager()的queryIntentActivities方法遍历 76 | List resolveinfoList = context.getPackageManager() 77 | .queryIntentActivities(resolveIntent, 0); 78 | 79 | ResolveInfo resolveinfo = resolveinfoList.iterator().next(); 80 | if (resolveinfo != null) { 81 | // packagename = 参数packname 82 | String packageName = resolveinfo.activityInfo.packageName; 83 | // 这个就是我们要找的该APP的LAUNCHER的Activity[组织形式:packagename.mainActivityname] 84 | String className = resolveinfo.activityInfo.name; 85 | // LAUNCHER Intent 86 | Intent intent = new Intent(Intent.ACTION_MAIN); 87 | intent.addCategory(Intent.CATEGORY_LAUNCHER); 88 | 89 | // 设置ComponentName 参数1:packagename 参数2:MainActivity路径 90 | ComponentName cn = new ComponentName(packageName, className); 91 | 92 | intent.setComponent(cn); 93 | context.startActivity(intent); 94 | } 95 | } 96 | 97 | /** 98 | * 获取apk下载存储的文件夹路径(不包含apkName) 99 | * 100 | * @param context 101 | * @return 102 | */ 103 | public static String getApkFileDir(Context context) { 104 | return FileUtils.newInstance(context).getTempPath().getAbsolutePath(); 105 | } 106 | 107 | /** 108 | * 安装应用 109 | * 110 | * @param context 111 | * @param apkPath 112 | */ 113 | public static void installApp(Context context, String apkPath) { 114 | Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE); 115 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 116 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 117 | Uri uri; 118 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 119 | uri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileProvider", new File(apkPath)); 120 | } else { 121 | uri = Uri.fromFile(new File(apkPath)); 122 | } 123 | intent.setDataAndType(uri, "application/vnd.android.package-archive"); 124 | context.startActivity(intent); 125 | } 126 | 127 | /** 128 | * 卸载应用,android 9.0失效,暂未解决 129 | * 130 | * @param context 131 | * @param packageName 132 | */ 133 | public static void unstallApp(Activity context, String packageName) { 134 | 135 | Intent uninstall_intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE); 136 | uninstall_intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 137 | uninstall_intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 138 | uninstall_intent.setData(Uri.parse("package:" + packageName)); 139 | context.startActivity(uninstall_intent); 140 | context.finish(); 141 | } 142 | 143 | /** 144 | * 从apkUrl中截取apkName,截取失败定为"temp.apk" 145 | * 146 | * @param apkUrl 147 | * @return 148 | */ 149 | public static String getApkName(String apkUrl) { 150 | if (TextUtils.isEmpty(apkUrl)) 151 | return null; 152 | String apkName = null; 153 | if (apkUrl.endsWith(".apk")) 154 | apkName = apkUrl.substring(apkUrl.lastIndexOf("/") + 1); 155 | if (TextUtils.isEmpty(apkName)) 156 | apkName = "temp.apk"; 157 | return apkName; 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /appupdate/src/main/java/com/hailong/appupdate/utils/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate.utils; 2 | 3 | import android.content.Context; 4 | import android.net.Uri; 5 | import android.os.Build; 6 | import android.os.Environment; 7 | 8 | import androidx.annotation.Nullable; 9 | import androidx.core.content.FileProvider; 10 | 11 | import java.io.File; 12 | import java.io.FileNotFoundException; 13 | import java.io.FileOutputStream; 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.util.Collections; 17 | import java.util.LinkedList; 18 | 19 | /** 20 | * Created by ZuoHailong on 2019/7/8. 21 | */ 22 | public class FileUtils { 23 | 24 | private static FileUtils fileUtils; 25 | 26 | private static String APPS_ROOT_DIR; 27 | private static final String IMAGE_PATH = "/Image"; 28 | private static final String TEMP_PATH = "/Temp"; 29 | private static final String APP_CRASH_PATH = "/AppCrash"; 30 | private static final String FILE_PATH = "/File"; 31 | 32 | public FileUtils(Context context) { 33 | APPS_ROOT_DIR = getExternalStorePath() + File.separator + context.getPackageName(); 34 | } 35 | 36 | public static FileUtils newInstance(Context context) { 37 | if (fileUtils == null) { 38 | synchronized (FileUtils.class) { 39 | if (fileUtils == null) { 40 | fileUtils = new FileUtils(context); 41 | } 42 | } 43 | } 44 | return fileUtils; 45 | } 46 | 47 | /** 48 | * 外置存储卡的路径 49 | * 50 | * @return 51 | */ 52 | @Nullable 53 | public static String getExternalStorePath() { 54 | if (isExistExternalStore()) { 55 | return Environment.getExternalStorageDirectory().getPath(); 56 | } 57 | return null; 58 | } 59 | 60 | /** 61 | * 是否有外存卡 62 | * 63 | * @return 64 | */ 65 | public static boolean isExistExternalStore() { 66 | return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); 67 | } 68 | 69 | 70 | /** 71 | * 返回图片存放目录 72 | * 73 | * @return File 74 | */ 75 | @Nullable 76 | public File getImagePath() { 77 | return create(APPS_ROOT_DIR + IMAGE_PATH); 78 | } 79 | 80 | /** 81 | * 返回临时存放目录 82 | * 83 | * @return File 84 | */ 85 | @Nullable 86 | public File getTempPath() { 87 | return create(APPS_ROOT_DIR + TEMP_PATH); 88 | } 89 | 90 | /** 91 | * 存储日志文件目录 92 | * 93 | * @return File 94 | */ 95 | public File getAppCrashPath() { 96 | return create(APPS_ROOT_DIR + APP_CRASH_PATH); 97 | } 98 | 99 | /** 100 | * 存储文件目录 101 | * 102 | * @return File 103 | */ 104 | public File getFilePath() { 105 | return create(APPS_ROOT_DIR + FILE_PATH); 106 | } 107 | 108 | /** 109 | * 7.0以上拍照 安装应用等文件问题 110 | * 111 | * @param context context 112 | * @param file file 113 | * @return Uri 114 | */ 115 | public static Uri getFileUri(Context context, File file) { 116 | Uri fileUri; 117 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 118 | fileUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", file); 119 | } else { 120 | fileUri = Uri.fromFile(file); 121 | } 122 | return fileUri; 123 | } 124 | 125 | /** 126 | * 获取应用下的.fileprovider的路径 127 | * 128 | * @param context 129 | * @return 130 | */ 131 | public static String getFileProviderName(Context context) { 132 | return context.getPackageName() + ".fileprovider"; 133 | } 134 | 135 | 136 | /** 137 | * 获取目录下的文件列表 138 | * 139 | * @param strPath strPath 140 | */ 141 | public static LinkedList listLinkedFiles(String strPath) { 142 | File dir = new File(strPath); 143 | File file[] = dir.listFiles(); 144 | LinkedList list = null; 145 | if (dir.exists() && file != null && file.length > 0) { 146 | list = new LinkedList<>(); 147 | Collections.addAll(list, file); 148 | } 149 | return list; 150 | } 151 | 152 | /** 153 | * 删除多个文件 154 | * 155 | * @param filesName filesName 156 | */ 157 | public static void deleteListFiles(String filesName) { 158 | LinkedList files = listLinkedFiles(filesName); 159 | if (files != null && files.size() > 0) { 160 | for (File file : files) { 161 | file.delete(); 162 | } 163 | } 164 | } 165 | 166 | /** 167 | * 删除指定文件 168 | * 169 | * @param filesName filesName 170 | */ 171 | public static void deleteFile(String filesName) { 172 | File file = new File(filesName); 173 | if (file.exists()) { 174 | file.delete(); 175 | } 176 | } 177 | 178 | /** 179 | * 将Assets中的文件拷贝到Sdcard指定路径 180 | * 181 | * @param context 182 | * @param assetsFileName 183 | * @param toPath Sdcard指定路径 184 | */ 185 | public static void copyAssetsToSdcard(Context context, String assetsFileName, String toPath) { 186 | try { 187 | boolean sdCardExist = Environment.getExternalStorageState().equals(android.os.Environment.MEDIA_MOUNTED); 188 | if (!sdCardExist) 189 | return; 190 | 191 | File dirFile = new File(toPath); 192 | if (!dirFile.exists()) 193 | dirFile.mkdirs(); 194 | 195 | File file = new File(toPath, assetsFileName); 196 | if (file.exists()) 197 | return; 198 | 199 | InputStream ins = context.getAssets().open(assetsFileName); 200 | FileOutputStream fos = new FileOutputStream(file); 201 | byte[] buffer = new byte[1024]; 202 | int length = 0; 203 | while ((length = ins.read(buffer)) != -1) { 204 | fos.write(buffer, 0, length); 205 | } 206 | fos.flush(); 207 | fos.close(); 208 | ins.close(); 209 | } catch (FileNotFoundException e) { 210 | e.printStackTrace(); 211 | } catch (IOException e) { 212 | e.printStackTrace(); 213 | } 214 | } 215 | 216 | private static File create(String path) { 217 | if (!isExistExternalStore()) { 218 | return null; 219 | } 220 | File directory = new File(path); 221 | if (!directory.exists()) { 222 | directory.mkdirs(); 223 | } 224 | return directory; 225 | } 226 | 227 | } 228 | -------------------------------------------------------------------------------- /appupdate/src/main/java/com/hailong/appupdate/utils/ImageUtil.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate.utils; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.renderscript.Allocation; 6 | import android.renderscript.Element; 7 | import android.renderscript.RenderScript; 8 | import android.renderscript.ScriptIntrinsicBlur; 9 | import android.view.View; 10 | 11 | /** 12 | * Created by ZuoHailong on 2019/7/8. 13 | */ 14 | public class ImageUtil { 15 | 16 | /** 17 | * 对View的可视部分进行截屏 18 | * 注意:要截取的区域一定要有背景色的设置,比如设置为白色(#ffffff),否则会出现截取后查看图片黑屏,或者分享到微信(QQ、纷享销客等)后黑屏的问题。 19 | */ 20 | public static Bitmap screenShotView(View view) { 21 | //开启缓存功能 22 | view.setDrawingCacheEnabled(true); 23 | //创建缓存 24 | view.buildDrawingCache(); 25 | //获取缓存Bitmap 26 | Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache()); 27 | return bitmap; 28 | } 29 | 30 | /** 31 | * 图片模糊化处理 32 | * 33 | * @param context 34 | * @param bitmap 待处理图片 35 | * @param width 模糊后的bitmap宽度 36 | * @param height 模糊后的bitmap高度 37 | * @return 38 | */ 39 | public static Bitmap blur(Context context, Bitmap bitmap, int width, int height) { 40 | 41 | //创建一个缩小后的bitmap 42 | Bitmap inputBitmap = Bitmap.createScaledBitmap(bitmap, width, height, false); 43 | //创建将在ondraw中使用到的经过模糊处理后的bitmap 44 | Bitmap outputBitmap = Bitmap.createBitmap(inputBitmap); 45 | 46 | //创建RenderScript,ScriptIntrinsicBlur固定写法 47 | RenderScript rs = RenderScript.create(context); 48 | ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs)); 49 | 50 | //根据inputBitmap,outputBitmap分别分配内存 51 | Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap); 52 | Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap); 53 | 54 | //设置模糊半径取值(0-25]之间,不同半径得到的模糊效果不同 55 | blurScript.setRadius(25); 56 | blurScript.setInput(tmpIn); 57 | blurScript.forEach(tmpOut); 58 | 59 | //得到最终的模糊bitmap 60 | tmpOut.copyTo(outputBitmap); 61 | 62 | return outputBitmap; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /appupdate/src/main/java/com/hailong/appupdate/utils/ViewUtil.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate.utils; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | import android.widget.BaseAdapter; 7 | import android.widget.GridView; 8 | import android.widget.ListView; 9 | 10 | /** 11 | * Created by ZuoHailong on 2019/7/8. 12 | */ 13 | public class ViewUtil { 14 | 15 | /** 16 | * 记录上次点击按钮的时间 17 | **/ 18 | private static long lastClickTime = 0; 19 | /** 20 | * 按钮连续点击最低间隔时间 单位:毫秒 21 | **/ 22 | private final static int CLICK_TIME = 1200; 23 | 24 | 25 | /** 26 | * 防止快速多次点击 27 | */ 28 | public static boolean fastDoubleClick() { 29 | if (System.currentTimeMillis() - lastClickTime <= CLICK_TIME) { 30 | return true; 31 | } 32 | lastClickTime = System.currentTimeMillis(); 33 | return false; 34 | } 35 | 36 | @SuppressLint("NewApi") 37 | public static void initGridViewMeasure(GridView gv, BaseAdapter adapter, int size) { 38 | int totalHeight = 0; 39 | if (adapter.getCount() % size == 0) { 40 | for (int i = 0, len = adapter.getCount(); i < len / size; i++) { 41 | // listAdapter.getCount()返回数据项的数目 42 | View listItem = adapter.getView(i, null, gv); 43 | // 计算子项View 的宽高 44 | listItem.measure(0, 0); 45 | // 统计所有子项的总高度 46 | totalHeight += listItem.getMeasuredHeight(); 47 | } 48 | } else { 49 | for (int i = 0, len = adapter.getCount(); i < (len / size) + 1; i++) { 50 | // listAdapter.getCount()返回数据项的数目 51 | View listItem = adapter.getView(i, null, gv); 52 | // 计算子项View 的宽高 53 | listItem.measure(0, 0); 54 | // 统计所有子项的总高度 55 | totalHeight += listItem.getMeasuredHeight(); 56 | } 57 | } 58 | ViewGroup.LayoutParams params = gv.getLayoutParams(); 59 | params.height = totalHeight + (gv.getVerticalSpacing() * ((adapter.getCount() - 1)) / size); 60 | // listView.getDividerHeight()获取子项间分隔符占用的高度 61 | // params.height最后得到整个ListView完整显示需要的高度 62 | gv.setLayoutParams(params); 63 | } 64 | 65 | //动态测量listview高度 66 | public static void initListViewMeasure(ListView lv, BaseAdapter adapter) { 67 | //动态测量listview高度 68 | int totalHeight = 0; 69 | for (int i = 0, len = adapter.getCount(); i < len; i++) { 70 | // listAdapter.getCount()返回数据项的数目 71 | View listItem = adapter.getView(i, null, lv); 72 | // 计算子项View 的宽高 73 | listItem.measure(0, 0); 74 | // 统计所有子项的总高度 75 | totalHeight += listItem.getMeasuredHeight(); 76 | } 77 | ViewGroup.LayoutParams params = lv.getLayoutParams(); 78 | params.height = totalHeight + (lv.getDividerHeight() * (adapter.getCount() - 1)); 79 | // listView.getDividerHeight()获取子项间分隔符占用的高度 80 | // params.height最后得到整个ListView完整显示需要的高度 81 | lv.setLayoutParams(params); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /appupdate/src/main/java/com/hailong/appupdate/utils/WeakHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Badoo Trading Limited 3 | * Permission is hereby granted, free of charge, to any person obtaining a copy 4 | * of this software and associated documentation files (the "Software"), to deal 5 | * in the Software without restriction, including without limitation the rights 6 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | * copies of the Software, and to permit persons to whom the Software is 8 | * furnished to do so, subject to the following conditions: 9 | * 10 | * The above copyright notice and this permission notice shall be included in 11 | * all copies or substantial portions of the Software. 12 | * 13 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | * THE SOFTWARE. 20 | * 21 | * Portions of documentation in this code are modifications based on work created and 22 | * shared by Android Open Source Project and used according to terms described in the 23 | * Apache License, Version 2.0 24 | */ 25 | package com.hailong.appupdate.utils; 26 | 27 | import android.os.Handler; 28 | import android.os.Looper; 29 | import android.os.Message; 30 | 31 | import androidx.annotation.NonNull; 32 | import androidx.annotation.Nullable; 33 | import androidx.annotation.VisibleForTesting; 34 | 35 | import java.lang.ref.WeakReference; 36 | import java.util.concurrent.locks.Lock; 37 | import java.util.concurrent.locks.ReentrantLock; 38 | 39 | /** 40 | * Memory safer implementation of android.os.Handler 41 | *

42 | * Original implementation of Handlers always keeps hard reference to handler in queue of execution. 43 | * If you create anonymous handler and post delayed message into it, it will keep all parent class 44 | * for that time in memory even if it could be cleaned. 45 | *

46 | * This implementation is trickier, it will keep WeakReferences to runnables and messages, 47 | * and GC could collect them once WeakHandler instance is not referenced any more 48 | *

49 | * 50 | * @see Handler 51 | *

52 | * Created by Dmytro Voronkevych on 17/06/2014. 53 | */ 54 | @SuppressWarnings("unused") 55 | public class WeakHandler { 56 | private final Handler.Callback mCallback; // hard reference to Callback. We need to keep callback in memory 57 | private final ExecHandler mExec; 58 | private Lock mLock = new ReentrantLock(); 59 | @SuppressWarnings("ConstantConditions") 60 | @VisibleForTesting 61 | final ChainedRef mRunnables = new ChainedRef(mLock, null); 62 | 63 | /** 64 | * Default constructor associates this handler bindDisposable the {@link Looper} for the 65 | * current thread. 66 | *

67 | * If this thread does not have a looper, this handler won't be able to receive messages 68 | * so an exception is thrown. 69 | */ 70 | public WeakHandler() { 71 | mCallback = null; 72 | mExec = new ExecHandler(); 73 | } 74 | 75 | /** 76 | * Constructor associates this handler bindDisposable the {@link Looper} for the 77 | * current thread and takes a callback interface in which you can handle 78 | * messages. 79 | *

80 | * If this thread does not have a looper, this handler won't be able to receive messages 81 | * so an exception is thrown. 82 | * 83 | * @param callback The callback interface in which to handle messages, or null. 84 | */ 85 | public WeakHandler(@Nullable Handler.Callback callback) { 86 | mCallback = callback; // Hard referencing body 87 | mExec = new ExecHandler(new WeakReference<>(callback)); // Weak referencing inside ExecHandler 88 | } 89 | 90 | /** 91 | * Use the provided {@link Looper} instead of the default one. 92 | * 93 | * @param looper The looper, must not be null. 94 | */ 95 | public WeakHandler(@NonNull Looper looper) { 96 | mCallback = null; 97 | mExec = new ExecHandler(looper); 98 | } 99 | 100 | /** 101 | * Use the provided {@link Looper} instead of the default one and take a callback 102 | * interface in which to handle messages. 103 | * 104 | * @param looper The looper, must not be null. 105 | * @param callback The callback interface in which to handle messages, or null. 106 | */ 107 | public WeakHandler(@NonNull Looper looper, @NonNull Handler.Callback callback) { 108 | mCallback = callback; 109 | mExec = new ExecHandler(looper, new WeakReference<>(callback)); 110 | } 111 | 112 | /** 113 | * Causes the Runnable r to be added to the message queue. 114 | * The runnable will be run on the thread to which this handler is 115 | * attached. 116 | * 117 | * @param r The Runnable that will be executed. 118 | * @return Returns true if the Runnable was successfully placed in to the 119 | * message queue. Returns false on failure, usually because the 120 | * looper processing the message queue is exiting. 121 | */ 122 | public final boolean post(@NonNull Runnable r) { 123 | return mExec.post(wrapRunnable(r)); 124 | } 125 | 126 | /** 127 | * Causes the Runnable r to be added to the message queue, to be run 128 | * at a specific time given by uptimeMillis. 129 | * The time-base is {@link android.os.SystemClock#uptimeMillis}. 130 | * The runnable will be run on the thread to which this handler is attached. 131 | * 132 | * @param r The Runnable that will be executed. 133 | * @param uptimeMillis The absolute time at which the callback should run, 134 | * using the {@link android.os.SystemClock#uptimeMillis} time-base. 135 | * @return Returns true if the Runnable was successfully placed in to the 136 | * message queue. Returns false on failure, usually because the 137 | * looper processing the message queue is exiting. Note that a 138 | * result of true does not mean the Runnable will be processed -- if 139 | * the looper is quit before the delivery time of the message 140 | * occurs then the message will be dropped. 141 | */ 142 | public final boolean postAtTime(@NonNull Runnable r, long uptimeMillis) { 143 | return mExec.postAtTime(wrapRunnable(r), uptimeMillis); 144 | } 145 | 146 | /** 147 | * Causes the Runnable r to be added to the message queue, to be run 148 | * at a specific time given by uptimeMillis. 149 | * The time-base is {@link android.os.SystemClock#uptimeMillis}. 150 | * The runnable will be run on the thread to which this handler is attached. 151 | * 152 | * @param r The Runnable that will be executed. 153 | * @param uptimeMillis The absolute time at which the callback should run, 154 | * using the {@link android.os.SystemClock#uptimeMillis} time-base. 155 | * @return Returns true if the Runnable was successfully placed in to the 156 | * message queue. Returns false on failure, usually because the 157 | * looper processing the message queue is exiting. Note that a 158 | * result of true does not mean the Runnable will be processed -- if 159 | * the looper is quit before the delivery time of the message 160 | * occurs then the message will be dropped. 161 | * @see android.os.SystemClock#uptimeMillis 162 | */ 163 | public final boolean postAtTime(Runnable r, Object token, long uptimeMillis) { 164 | return mExec.postAtTime(wrapRunnable(r), token, uptimeMillis); 165 | } 166 | 167 | /** 168 | * Causes the Runnable r to be added to the message queue, to be run 169 | * after the specified amount of time elapses. 170 | * The runnable will be run on the thread to which this handler 171 | * is attached. 172 | * 173 | * @param r The Runnable that will be executed. 174 | * @param delayMillis The delay (in milliseconds) until the Runnable 175 | * will be executed. 176 | * @return Returns true if the Runnable was successfully placed in to the 177 | * message queue. Returns false on failure, usually because the 178 | * looper processing the message queue is exiting. Note that a 179 | * result of true does not mean the Runnable will be processed -- 180 | * if the looper is quit before the delivery time of the message 181 | * occurs then the message will be dropped. 182 | */ 183 | public final boolean postDelayed(Runnable r, long delayMillis) { 184 | return mExec.postDelayed(wrapRunnable(r), delayMillis); 185 | } 186 | 187 | /** 188 | * Posts a message to an object that implements Runnable. 189 | * Causes the Runnable r to executed on the next iteration through the 190 | * message queue. The runnable will be run on the thread to which this 191 | * handler is attached. 192 | * This method is only for use in very special circumstances -- it 193 | * can easily starve the message queue, cause ordering problems, or have 194 | * other unexpected side-effects. 195 | * 196 | * @param r The Runnable that will be executed. 197 | * @return Returns true if the message was successfully placed in to the 198 | * message queue. Returns false on failure, usually because the 199 | * looper processing the message queue is exiting. 200 | */ 201 | public final boolean postAtFrontOfQueue(Runnable r) { 202 | return mExec.postAtFrontOfQueue(wrapRunnable(r)); 203 | } 204 | 205 | /** 206 | * Remove any pending posts of Runnable r that are in the message queue. 207 | */ 208 | public final void removeCallbacks(Runnable r) { 209 | final WeakRunnable runnable = mRunnables.remove(r); 210 | if (runnable != null) { 211 | mExec.removeCallbacks(runnable); 212 | } 213 | } 214 | 215 | /** 216 | * Remove any pending posts of Runnable r bindDisposable Object 217 | * token that are in the message queue. If token is null, 218 | * all callbacks will be removed. 219 | */ 220 | public final void removeCallbacks(Runnable r, Object token) { 221 | final WeakRunnable runnable = mRunnables.remove(r); 222 | if (runnable != null) { 223 | mExec.removeCallbacks(runnable, token); 224 | } 225 | } 226 | 227 | /** 228 | * Pushes a message onto the end of the message queue after all pending messages 229 | * before the current time. It will be received in callback, 230 | * in the thread attached to this handler. 231 | * 232 | * @return Returns true if the message was successfully placed in to the 233 | * message queue. Returns false on failure, usually because the 234 | * looper processing the message queue is exiting. 235 | */ 236 | public final boolean sendMessage(Message msg) { 237 | return mExec.sendMessage(msg); 238 | } 239 | 240 | /** 241 | * Sends a Message containing only the what value. 242 | * 243 | * @return Returns true if the message was successfully placed in to the 244 | * message queue. Returns false on failure, usually because the 245 | * looper processing the message queue is exiting. 246 | */ 247 | public final boolean sendEmptyMessage(int what) { 248 | return mExec.sendEmptyMessage(what); 249 | } 250 | 251 | /** 252 | * Sends a Message containing only the what value, to be delivered 253 | * after the specified amount of time elapses. 254 | * 255 | * @return Returns true if the message was successfully placed in to the 256 | * message queue. Returns false on failure, usually because the 257 | * looper processing the message queue is exiting. 258 | * @see #sendMessageDelayed(Message, long) 259 | */ 260 | public final boolean sendEmptyMessageDelayed(int what, long delayMillis) { 261 | return mExec.sendEmptyMessageDelayed(what, delayMillis); 262 | } 263 | 264 | /** 265 | * Sends a Message containing only the what value, to be delivered 266 | * at a specific time. 267 | * 268 | * @return Returns true if the message was successfully placed in to the 269 | * message queue. Returns false on failure, usually because the 270 | * looper processing the message queue is exiting. 271 | * @see #sendMessageAtTime(Message, long) 272 | */ 273 | public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis) { 274 | return mExec.sendEmptyMessageAtTime(what, uptimeMillis); 275 | } 276 | 277 | /** 278 | * Enqueue a message into the message queue after all pending messages 279 | * before (current time + delayMillis). You will receive it in 280 | * callback, in the thread attached to this handler. 281 | * 282 | * @return Returns true if the message was successfully placed in to the 283 | * message queue. Returns false on failure, usually because the 284 | * looper processing the message queue is exiting. Note that a 285 | * result of true does not mean the message will be processed -- if 286 | * the looper is quit before the delivery time of the message 287 | * occurs then the message will be dropped. 288 | */ 289 | public final boolean sendMessageDelayed(Message msg, long delayMillis) { 290 | return mExec.sendMessageDelayed(msg, delayMillis); 291 | } 292 | 293 | /** 294 | * Enqueue a message into the message queue after all pending messages 295 | * before the absolute time (in milliseconds) uptimeMillis. 296 | * The time-base is {@link android.os.SystemClock#uptimeMillis}. 297 | * You will receive it in callback, in the thread attached 298 | * to this handler. 299 | * 300 | * @param uptimeMillis The absolute time at which the message should be 301 | * delivered, using the 302 | * {@link android.os.SystemClock#uptimeMillis} time-base. 303 | * @return Returns true if the message was successfully placed in to the 304 | * message queue. Returns false on failure, usually because the 305 | * looper processing the message queue is exiting. Note that a 306 | * result of true does not mean the message will be processed -- if 307 | * the looper is quit before the delivery time of the message 308 | * occurs then the message will be dropped. 309 | */ 310 | public boolean sendMessageAtTime(Message msg, long uptimeMillis) { 311 | return mExec.sendMessageAtTime(msg, uptimeMillis); 312 | } 313 | 314 | /** 315 | * Enqueue a message at the front of the message queue, to be processed on 316 | * the next iteration of the message loop. You will receive it in 317 | * callback, in the thread attached to this handler. 318 | * This method is only for use in very special circumstances -- it 319 | * can easily starve the message queue, cause ordering problems, or have 320 | * other unexpected side-effects. 321 | * 322 | * @return Returns true if the message was successfully placed in to the 323 | * message queue. Returns false on failure, usually because the 324 | * looper processing the message queue is exiting. 325 | */ 326 | public final boolean sendMessageAtFrontOfQueue(Message msg) { 327 | return mExec.sendMessageAtFrontOfQueue(msg); 328 | } 329 | 330 | /** 331 | * Remove any pending posts of messages bindDisposable code 'what' that are in the 332 | * message queue. 333 | */ 334 | public final void removeMessages(int what) { 335 | mExec.removeMessages(what); 336 | } 337 | 338 | /** 339 | * Remove any pending posts of messages bindDisposable code 'what' and whose obj is 340 | * 'object' that are in the message queue. If object is null, 341 | * all messages will be removed. 342 | */ 343 | public final void removeMessages(int what, Object object) { 344 | mExec.removeMessages(what, object); 345 | } 346 | 347 | /** 348 | * Remove any pending posts of callbacks and sent messages whose 349 | * obj is token. If token is null, 350 | * all callbacks and messages will be removed. 351 | */ 352 | public final void removeCallbacksAndMessages(Object token) { 353 | mExec.removeCallbacksAndMessages(token); 354 | } 355 | 356 | /** 357 | * Check if there are any pending posts of messages bindDisposable code 'what' in 358 | * the message queue. 359 | */ 360 | public final boolean hasMessages(int what) { 361 | return mExec.hasMessages(what); 362 | } 363 | 364 | /** 365 | * Check if there are any pending posts of messages bindDisposable code 'what' and 366 | * whose obj is 'object' in the message queue. 367 | */ 368 | public final boolean hasMessages(int what, Object object) { 369 | return mExec.hasMessages(what, object); 370 | } 371 | 372 | public final Looper getLooper() { 373 | return mExec.getLooper(); 374 | } 375 | 376 | private WeakRunnable wrapRunnable(@NonNull Runnable r) { 377 | //noinspection ConstantConditions 378 | if (r == null) { 379 | throw new NullPointerException("Runnable can't be null"); 380 | } 381 | final ChainedRef hardRef = new ChainedRef(mLock, r); 382 | mRunnables.insertAfter(hardRef); 383 | return hardRef.wrapper; 384 | } 385 | 386 | private static class ExecHandler extends Handler { 387 | private final WeakReference mCallback; 388 | 389 | ExecHandler() { 390 | mCallback = null; 391 | } 392 | 393 | ExecHandler(WeakReference callback) { 394 | mCallback = callback; 395 | } 396 | 397 | ExecHandler(Looper looper) { 398 | super(looper); 399 | mCallback = null; 400 | } 401 | 402 | ExecHandler(Looper looper, WeakReference callback) { 403 | super(looper); 404 | mCallback = callback; 405 | } 406 | 407 | @Override 408 | public void handleMessage(@NonNull Message msg) { 409 | if (mCallback == null) { 410 | return; 411 | } 412 | final Callback callback = mCallback.get(); 413 | if (callback == null) { // Already disposed 414 | return; 415 | } 416 | callback.handleMessage(msg); 417 | } 418 | } 419 | 420 | static class WeakRunnable implements Runnable { 421 | private final WeakReference mDelegate; 422 | private final WeakReference mReference; 423 | 424 | WeakRunnable(WeakReference delegate, WeakReference reference) { 425 | mDelegate = delegate; 426 | mReference = reference; 427 | } 428 | 429 | @Override 430 | public void run() { 431 | final Runnable delegate = mDelegate.get(); 432 | final ChainedRef reference = mReference.get(); 433 | if (reference != null) { 434 | reference.remove(); 435 | } 436 | if (delegate != null) { 437 | delegate.run(); 438 | } 439 | } 440 | } 441 | 442 | static class ChainedRef { 443 | @Nullable 444 | ChainedRef next; 445 | @Nullable 446 | ChainedRef prev; 447 | @NonNull 448 | final Runnable runnable; 449 | @NonNull 450 | final WeakRunnable wrapper; 451 | 452 | @NonNull 453 | Lock lock; 454 | 455 | public ChainedRef(@NonNull Lock lock, @NonNull Runnable r) { 456 | this.runnable = r; 457 | this.lock = lock; 458 | this.wrapper = new WeakRunnable(new WeakReference<>(r), new WeakReference<>(this)); 459 | } 460 | 461 | public WeakRunnable remove() { 462 | lock.lock(); 463 | try { 464 | if (prev != null) { 465 | prev.next = next; 466 | } 467 | if (next != null) { 468 | next.prev = prev; 469 | } 470 | prev = null; 471 | next = null; 472 | } finally { 473 | lock.unlock(); 474 | } 475 | return wrapper; 476 | } 477 | 478 | public void insertAfter(@NonNull ChainedRef candidate) { 479 | lock.lock(); 480 | try { 481 | if (this.next != null) { 482 | this.next.prev = candidate; 483 | } 484 | 485 | candidate.next = this.next; 486 | this.next = candidate; 487 | candidate.prev = this; 488 | } finally { 489 | lock.unlock(); 490 | } 491 | } 492 | 493 | @Nullable 494 | public WeakRunnable remove(Runnable obj) { 495 | lock.lock(); 496 | try { 497 | ChainedRef curr = this.next; // Skipping head 498 | while (curr != null) { 499 | if (curr.runnable == obj) { // We do comparison exactly how Handler does inside 500 | return curr.remove(); 501 | } 502 | curr = curr.next; 503 | } 504 | } finally { 505 | lock.unlock(); 506 | } 507 | return null; 508 | } 509 | } 510 | } 511 | -------------------------------------------------------------------------------- /appupdate/src/main/java/com/hailong/appupdate/view/recyclerview/CommonRecycleViewAdapter.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate.view.recyclerview; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.LinearLayout; 8 | 9 | import androidx.recyclerview.widget.RecyclerView; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * @author ZuoHailong 15 | * @date 2019/4/3. 16 | */ 17 | public abstract class CommonRecycleViewAdapter extends RecyclerView.Adapter { 18 | protected Context mContext; 19 | protected int mLayoutId; 20 | protected List mDatas; 21 | protected LayoutInflater mInflater; 22 | private OnItemClickListener mItemClickListener; 23 | private onLongItemClickListener mLongItemClickListener; 24 | 25 | public CommonRecycleViewAdapter() { 26 | } 27 | 28 | public CommonRecycleViewAdapter(Context context, int layoutId, List datas) { 29 | mContext = context; 30 | mInflater = LayoutInflater.from(context); 31 | mLayoutId = layoutId; 32 | mDatas = datas; 33 | } 34 | 35 | @Override 36 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 37 | ViewHolder viewHolder = ViewHolder.get(mContext, parent, mLayoutId); 38 | return viewHolder; 39 | } 40 | 41 | @Override 42 | public void onBindViewHolder(final ViewHolder holder, final int position) { 43 | // holder.updatePosition(position); 44 | ViewGroup.LayoutParams layoutParams = holder.itemView.getLayoutParams(); 45 | layoutParams.height = LinearLayout.LayoutParams.WRAP_CONTENT; 46 | convert(holder, mDatas.get(position), position); 47 | if (mItemClickListener != null) { 48 | holder.itemView.setOnClickListener(new View.OnClickListener() { 49 | @Override 50 | public void onClick(View v) { 51 | mItemClickListener.onItemClick(v, position); 52 | } 53 | }); 54 | } 55 | if (mLongItemClickListener != null) { 56 | holder.itemView.setOnLongClickListener(new View.OnLongClickListener() { 57 | @Override 58 | public boolean onLongClick(View v) { 59 | mLongItemClickListener.onLongItemClick(v, position); 60 | return true; 61 | } 62 | }); 63 | } 64 | } 65 | 66 | public abstract void convert(ViewHolder holder, T t, int position); 67 | 68 | @Override 69 | public int getItemCount() { 70 | return mDatas.size(); 71 | } 72 | 73 | public void update(List datas) { 74 | mDatas = datas; 75 | notifyDataSetChanged(); 76 | } 77 | 78 | public void updateIndex(int positon, T t) { 79 | mDatas.set(positon, t); 80 | notifyDataSetChanged(); 81 | } 82 | 83 | public interface OnItemClickListener { 84 | void onItemClick(View view, int position); 85 | } 86 | 87 | public interface onLongItemClickListener { 88 | void onLongItemClick(View view, int position); 89 | } 90 | 91 | public void setOnItemClickListener(OnItemClickListener listener) { 92 | this.mItemClickListener = listener; 93 | } 94 | 95 | public void setonLongItemClickListener(onLongItemClickListener listener) { 96 | this.mLongItemClickListener = listener; 97 | } 98 | 99 | public OnItemClickListener getmItemClickListener() { 100 | return mItemClickListener; 101 | } 102 | 103 | public onLongItemClickListener getmLongItemClickListener() { 104 | return mLongItemClickListener; 105 | } 106 | } 107 | 108 | -------------------------------------------------------------------------------- /appupdate/src/main/java/com/hailong/appupdate/view/recyclerview/DividerGridItemDecoration.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate.view.recyclerview; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Rect; 7 | import android.graphics.drawable.Drawable; 8 | import android.view.View; 9 | 10 | import androidx.recyclerview.widget.GridLayoutManager; 11 | import androidx.recyclerview.widget.RecyclerView; 12 | import androidx.recyclerview.widget.StaggeredGridLayoutManager; 13 | 14 | /** 15 | * @author ZuoHailong 16 | * @date 2019/4/4. 17 | */ 18 | public class DividerGridItemDecoration extends RecyclerView.ItemDecoration { 19 | 20 | private static final int[] ATTRS = new int[]{android.R.attr.listDivider}; 21 | private Drawable mDivider; 22 | 23 | public DividerGridItemDecoration(Context context) { 24 | final TypedArray a = context.obtainStyledAttributes(ATTRS); 25 | mDivider = a.getDrawable(0); 26 | a.recycle(); 27 | } 28 | 29 | @Override 30 | public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) { 31 | 32 | drawHorizontal(c, parent); 33 | drawVertical(c, parent); 34 | 35 | } 36 | 37 | private int getSpanCount(RecyclerView parent) { 38 | // 列数 39 | int spanCount = -1; 40 | RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); 41 | if (layoutManager instanceof GridLayoutManager) { 42 | 43 | spanCount = ((GridLayoutManager) layoutManager).getSpanCount(); 44 | } else if (layoutManager instanceof StaggeredGridLayoutManager) { 45 | spanCount = ((StaggeredGridLayoutManager) layoutManager) 46 | .getSpanCount(); 47 | } 48 | return spanCount; 49 | } 50 | 51 | public void drawHorizontal(Canvas c, RecyclerView parent) { 52 | int childCount = parent.getChildCount(); 53 | for (int i = 0; i < childCount; i++) { 54 | final View child = parent.getChildAt(i); 55 | final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child 56 | .getLayoutParams(); 57 | final int left = child.getLeft() - params.leftMargin; 58 | final int right = child.getRight() + params.rightMargin 59 | + mDivider.getIntrinsicWidth(); 60 | final int top = child.getBottom() + params.bottomMargin; 61 | final int bottom = top + mDivider.getIntrinsicHeight(); 62 | mDivider.setBounds(left, top, right, bottom); 63 | mDivider.draw(c); 64 | } 65 | } 66 | 67 | public void drawVertical(Canvas c, RecyclerView parent) { 68 | final int childCount = parent.getChildCount(); 69 | for (int i = 0; i < childCount; i++) { 70 | final View child = parent.getChildAt(i); 71 | 72 | final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child 73 | .getLayoutParams(); 74 | final int top = child.getTop() - params.topMargin; 75 | final int bottom = child.getBottom() + params.bottomMargin; 76 | final int left = child.getRight() + params.rightMargin; 77 | final int right = left + mDivider.getIntrinsicWidth(); 78 | 79 | mDivider.setBounds(left, top, right, bottom); 80 | mDivider.draw(c); 81 | } 82 | } 83 | 84 | private boolean isLastColum(RecyclerView parent, int pos, int spanCount, 85 | int childCount) { 86 | RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); 87 | if (layoutManager instanceof GridLayoutManager) { 88 | if ((pos + 1) % spanCount == 0)// 如果是最后一列,则不需要绘制右边 89 | { 90 | return true; 91 | } 92 | } else if (layoutManager instanceof StaggeredGridLayoutManager) { 93 | int orientation = ((StaggeredGridLayoutManager) layoutManager) 94 | .getOrientation(); 95 | if (orientation == StaggeredGridLayoutManager.VERTICAL) { 96 | if ((pos + 1) % spanCount == 0)// 如果是最后一列,则不需要绘制右边 97 | { 98 | return true; 99 | } 100 | } else { 101 | childCount = childCount - childCount % spanCount; 102 | if (pos >= childCount)// 如果是最后一列,则不需要绘制右边 103 | return true; 104 | } 105 | } 106 | return false; 107 | } 108 | 109 | private boolean isLastRaw(RecyclerView parent, int pos, int spanCount, 110 | int childCount) { 111 | RecyclerView.LayoutManager layoutManager = parent.getLayoutManager(); 112 | if (layoutManager instanceof GridLayoutManager) { 113 | childCount = childCount - childCount % spanCount; 114 | if (pos >= childCount)// 如果是最后一行,则不需要绘制底部 115 | return true; 116 | } else if (layoutManager instanceof StaggeredGridLayoutManager) { 117 | int orientation = ((StaggeredGridLayoutManager) layoutManager) 118 | .getOrientation(); 119 | // StaggeredGridLayoutManager 且纵向滚动 120 | if (orientation == StaggeredGridLayoutManager.VERTICAL) { 121 | childCount = childCount - childCount % spanCount; 122 | // 如果是最后一行,则不需要绘制底部 123 | if (pos >= childCount) 124 | return true; 125 | } else 126 | // StaggeredGridLayoutManager 且横向滚动 127 | { 128 | // 如果是最后一行,则不需要绘制底部 129 | if ((pos + 1) % spanCount == 0) { 130 | return true; 131 | } 132 | } 133 | } 134 | return false; 135 | } 136 | 137 | @Override 138 | public void getItemOffsets(Rect outRect, int itemPosition, 139 | RecyclerView parent) { 140 | int spanCount = getSpanCount(parent); 141 | int childCount = parent.getAdapter().getItemCount(); 142 | if (isLastRaw(parent, itemPosition, spanCount, childCount))// 如果是最后一行,则不需要绘制底部 143 | { 144 | outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); 145 | } else if (isLastColum(parent, itemPosition, spanCount, childCount))// 如果是最后一列,则不需要绘制右边 146 | { 147 | outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); 148 | } else { 149 | outRect.set(0, 0, mDivider.getIntrinsicWidth(), 150 | mDivider.getIntrinsicHeight()); 151 | } 152 | } 153 | } -------------------------------------------------------------------------------- /appupdate/src/main/java/com/hailong/appupdate/view/recyclerview/DividerItemDecoration.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate.view.recyclerview; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Rect; 7 | import android.graphics.drawable.Drawable; 8 | import android.view.View; 9 | 10 | import androidx.recyclerview.widget.LinearLayoutManager; 11 | import androidx.recyclerview.widget.RecyclerView; 12 | 13 | /** 14 | * @author ZuoHailong 15 | * @date 2019/4/4. 16 | */ 17 | public class DividerItemDecoration extends RecyclerView.ItemDecoration { 18 | 19 | private static final int[] ATTRS = new int[]{ 20 | android.R.attr.listDivider 21 | }; 22 | 23 | public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL; 24 | 25 | public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL; 26 | 27 | private Drawable mDivider; 28 | 29 | private int mOrientation; 30 | 31 | public DividerItemDecoration(Context context, int orientation) { 32 | final TypedArray a = context.obtainStyledAttributes(ATTRS); 33 | mDivider = a.getDrawable(0); 34 | a.recycle(); 35 | setOrientation(orientation); 36 | } 37 | 38 | public void setOrientation(int orientation) { 39 | if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST) { 40 | throw new IllegalArgumentException("invalid orientation"); 41 | } 42 | mOrientation = orientation; 43 | } 44 | 45 | @Override 46 | public void onDraw(Canvas c, RecyclerView parent) { 47 | if (mOrientation == VERTICAL_LIST) { 48 | drawVertical(c, parent); 49 | } else { 50 | drawHorizontal(c, parent); 51 | } 52 | 53 | } 54 | 55 | 56 | public void drawVertical(Canvas c, RecyclerView parent) { 57 | final int left = parent.getPaddingLeft(); 58 | final int right = parent.getWidth() - parent.getPaddingRight(); 59 | 60 | final int childCount = parent.getChildCount(); 61 | for (int i = 0; i < childCount; i++) { 62 | final View child = parent.getChildAt(i); 63 | RecyclerView v = new RecyclerView(parent.getContext()); 64 | final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child 65 | .getLayoutParams(); 66 | final int top = child.getBottom() + params.bottomMargin; 67 | final int bottom = top + mDivider.getIntrinsicHeight(); 68 | mDivider.setBounds(left, top, right, bottom); 69 | mDivider.draw(c); 70 | } 71 | } 72 | 73 | public void drawHorizontal(Canvas c, RecyclerView parent) { 74 | final int top = parent.getPaddingTop(); 75 | final int bottom = parent.getHeight() - parent.getPaddingBottom(); 76 | 77 | final int childCount = parent.getChildCount(); 78 | for (int i = 0; i < childCount; i++) { 79 | final View child = parent.getChildAt(i); 80 | final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child 81 | .getLayoutParams(); 82 | final int left = child.getRight() + params.rightMargin; 83 | final int right = left + mDivider.getIntrinsicHeight(); 84 | mDivider.setBounds(left, top, right, bottom); 85 | mDivider.draw(c); 86 | } 87 | } 88 | 89 | @Override 90 | public void getItemOffsets(Rect outRect, int itemPosition, RecyclerView parent) { 91 | if (mOrientation == VERTICAL_LIST) { 92 | outRect.set(0, 0, 0, mDivider.getIntrinsicHeight()); 93 | } else { 94 | outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /appupdate/src/main/java/com/hailong/appupdate/view/recyclerview/GridItemSpaceDecoration.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate.view.recyclerview; 2 | 3 | import android.graphics.Rect; 4 | import android.view.View; 5 | import android.view.ViewGroup; 6 | 7 | import androidx.recyclerview.widget.GridLayoutManager; 8 | import androidx.recyclerview.widget.RecyclerView; 9 | 10 | /** 11 | * Describe: 12 | * Created by ZuoHailong on 2019/4/25. 13 | */ 14 | public class GridItemSpaceDecoration extends RecyclerView.ItemDecoration { 15 | private static final int DEFAULT_COLUMN = Integer.MAX_VALUE; 16 | private int space; 17 | private int column; 18 | 19 | public GridItemSpaceDecoration(int space, int column) { 20 | this.space = space; 21 | this.column = column; 22 | } 23 | 24 | @Override 25 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { 26 | outRect.top = space; 27 | int pos = parent.getChildLayoutPosition(view); 28 | int total = parent.getChildCount(); 29 | if (isFirstRow(pos)) { 30 | outRect.top = 0; 31 | } 32 | if (isLastRow(pos, total)) { 33 | outRect.bottom = 5; 34 | } 35 | if (column != DEFAULT_COLUMN) { 36 | float avg = (column - 1) * space * 1.0f / column; 37 | outRect.left = (int) (pos % column * (space - avg)); 38 | outRect.right = (int) (avg - (pos % column * (space - avg))); 39 | } 40 | 41 | GridLayoutManager gridLayoutManager = (GridLayoutManager) parent.getLayoutManager(); 42 | ViewGroup.LayoutParams params = view.getLayoutParams(); 43 | params.height = gridLayoutManager.getWidth() / gridLayoutManager.getSpanCount() - view.getPaddingLeft() - view.getPaddingRight(); 44 | view.setLayoutParams(params); 45 | } 46 | 47 | boolean isFirstRow(int pos) { 48 | return pos < column; 49 | } 50 | 51 | boolean isLastRow(int pos, int total) { 52 | return total - pos <= column; 53 | } 54 | 55 | boolean isFirstColumn(int pos) { 56 | return pos % column == 0; 57 | } 58 | 59 | boolean isSecondColumn(int pos) { 60 | return isFirstColumn(pos - 1); 61 | } 62 | 63 | boolean isEndColumn(int pos) { 64 | return isFirstColumn(pos + 1); 65 | } 66 | 67 | boolean isNearEndColumn(int pos) { 68 | return isEndColumn(pos + 1); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /appupdate/src/main/java/com/hailong/appupdate/view/recyclerview/MaxHeightRecyclerView.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate.view.recyclerview; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.util.AttributeSet; 6 | 7 | import androidx.recyclerview.widget.RecyclerView; 8 | 9 | import com.hailong.appupdate.R; 10 | 11 | /** 12 | * Created by ZuoHailong on 2019/10/18. 13 | */ 14 | public class MaxHeightRecyclerView extends RecyclerView { 15 | private int mMaxHeight; 16 | 17 | public MaxHeightRecyclerView(Context context) { 18 | super(context); 19 | } 20 | 21 | public MaxHeightRecyclerView(Context context, AttributeSet attrs) { 22 | super(context, attrs); 23 | initialize(context, attrs); 24 | } 25 | 26 | public MaxHeightRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) { 27 | super(context, attrs, defStyleAttr); 28 | initialize(context, attrs); 29 | } 30 | 31 | private void initialize(Context context, AttributeSet attrs) { 32 | TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.appupdate_MaxHeightRecyclerView); 33 | mMaxHeight = arr.getLayoutDimension(R.styleable.appupdate_MaxHeightRecyclerView_appupdate_maxHeight, mMaxHeight); 34 | arr.recycle(); 35 | } 36 | 37 | @Override 38 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 39 | if (mMaxHeight > 0) { 40 | heightMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxHeight, MeasureSpec.AT_MOST); 41 | } 42 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /appupdate/src/main/java/com/hailong/appupdate/view/recyclerview/ViewHolder.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate.view.recyclerview; 2 | 3 | import android.content.Context; 4 | import android.util.SparseArray; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ImageView; 9 | import android.widget.TextView; 10 | 11 | import androidx.recyclerview.widget.RecyclerView; 12 | 13 | /** 14 | * Created by lry on 2018/3/20. 15 | */ 16 | 17 | public class ViewHolder extends RecyclerView.ViewHolder { 18 | 19 | private SparseArray mViews; 20 | private View mConvertView; 21 | private Context mContext; 22 | 23 | public ViewHolder(Context context, View itemView, ViewGroup parent) 24 | { 25 | super(itemView); 26 | mContext = context; 27 | mConvertView = itemView; 28 | mViews = new SparseArray(); 29 | } 30 | 31 | 32 | public static ViewHolder get(Context context, ViewGroup parent, int layoutId) 33 | { 34 | 35 | View itemView = LayoutInflater.from(context).inflate(layoutId, parent, 36 | false); 37 | ViewHolder holder = new ViewHolder(context, itemView, parent); 38 | return holder; 39 | } 40 | 41 | 42 | /** 43 | * 通过viewId获取控件 44 | * 45 | * @param viewId 46 | * @return 47 | */ 48 | public T getView(int viewId) 49 | { 50 | View view = mViews.get(viewId); 51 | if (view == null) 52 | { 53 | view = mConvertView.findViewById(viewId); 54 | mViews.put(viewId, view); 55 | } 56 | return (T) view; 57 | } 58 | 59 | /*==========辅助方法===========*/ 60 | public ViewHolder setText(int viewId, String text) 61 | { 62 | TextView tv = getView(viewId); 63 | tv.setText(text); 64 | return this; 65 | } 66 | 67 | public ViewHolder setImageResource(int viewId, int resId) 68 | { 69 | ImageView view = getView(viewId); 70 | view.setImageResource(resId); 71 | return this; 72 | } 73 | 74 | public ViewHolder setOnItemClickListener(int viewId,View.OnClickListener listener) 75 | { 76 | View view = getView(viewId); 77 | view.setOnClickListener(listener); 78 | return this; 79 | } 80 | public ViewHolder setOnLongClickListener(int viewId, View.OnLongClickListener listener) 81 | { 82 | View view = getView(viewId); 83 | view.setOnLongClickListener(listener); 84 | return this; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /appupdate/src/main/java/com/hailong/appupdate/widget/UpdateDialog.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate.widget; 2 | 3 | import android.Manifest; 4 | import android.app.AlertDialog; 5 | import android.app.DialogFragment; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.content.pm.PackageManager; 9 | import android.graphics.Color; 10 | import android.graphics.drawable.BitmapDrawable; 11 | import android.graphics.drawable.ColorDrawable; 12 | import android.net.Uri; 13 | import android.os.Build; 14 | import android.os.Bundle; 15 | import android.os.Handler; 16 | import android.os.Message; 17 | import android.provider.Settings; 18 | import android.text.TextUtils; 19 | import android.view.LayoutInflater; 20 | import android.view.View; 21 | import android.view.ViewGroup; 22 | import android.widget.ImageView; 23 | import android.widget.ProgressBar; 24 | import android.widget.RelativeLayout; 25 | import android.widget.TextView; 26 | 27 | import androidx.annotation.NonNull; 28 | import androidx.annotation.Nullable; 29 | import androidx.constraintlayout.widget.Group; 30 | import androidx.core.content.ContextCompat; 31 | import androidx.recyclerview.widget.LinearLayoutManager; 32 | 33 | import com.hailong.appupdate.R; 34 | import com.hailong.appupdate.utils.ApkUtil; 35 | import com.hailong.appupdate.utils.ImageUtil; 36 | import com.hailong.appupdate.utils.ViewUtil; 37 | import com.hailong.appupdate.utils.WeakHandler; 38 | import com.hailong.appupdate.view.recyclerview.CommonRecycleViewAdapter; 39 | import com.hailong.appupdate.view.recyclerview.MaxHeightRecyclerView; 40 | import com.hailong.appupdate.view.recyclerview.ViewHolder; 41 | import com.yanzhenjie.kalle.Kalle; 42 | import com.yanzhenjie.kalle.download.Callback; 43 | 44 | import java.util.ArrayList; 45 | import java.util.List; 46 | 47 | /** 48 | * Describe:更新询问框 49 | * Created by ZuoHailong on 2019/5/25. 50 | */ 51 | public class UpdateDialog extends DialogFragment implements View.OnClickListener { 52 | 53 | private static final int MSG_WHAT_PROGRESS = 101; 54 | private static final int MSG_WHAT_REQUEST_PERMISSION_SET = 102; 55 | private static final int MSG_WHAT_DOWNLOAD_START = 103; 56 | 57 | private ImageView ivTop; 58 | private TextView tvTitle, tvNewVerName, tvConfirm, tvCancle, tvDownloadStatus, tvProgress; 59 | private RelativeLayout layoutContent; 60 | private Group groupProgress; 61 | private MaxHeightRecyclerView recyclerView; 62 | private ProgressBar progressBar; 63 | 64 | private static Context context; 65 | private static UpdateDialog updateDialog; 66 | 67 | private boolean isForce; 68 | private String[] content; 69 | private CommonRecycleViewAdapter adapter; 70 | private String title, newVerName, apkUrl, confirmText, cancleText; 71 | private int topResId, confirmBgColor, cancelBgColor, confirmBgResource, cancelBgResource, progressDrawable; 72 | 73 | @Nullable 74 | @Override 75 | public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) { 76 | setCancelable(false); 77 | getDialog().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 78 | View view = inflater.inflate(R.layout.appupdate_dialogfrag_update, container); 79 | initView(view); 80 | return view; 81 | } 82 | 83 | @Override 84 | public void onClick(View v) { 85 | if (v.getId() == R.id.tvConfirm) {//立即更新 86 | if (ViewUtil.fastDoubleClick()) 87 | return; 88 | tvDownloadStatus.setVisibility(View.GONE); 89 | /*获取写入权限*/ 90 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {// >= 6.0 91 | if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 92 | requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1); 93 | return; 94 | } 95 | } 96 | update(); 97 | } else if (v.getId() == R.id.tvCancle) {//暂不更新 98 | dismiss(); 99 | tvDownloadStatus.setVisibility(View.GONE); 100 | } 101 | } 102 | 103 | @Override 104 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { 105 | if (requestCode == 1) { 106 | if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 107 | update(); 108 | } else { // 权限被拒绝 109 | handler.postDelayed(() -> handler.sendEmptyMessage(MSG_WHAT_REQUEST_PERMISSION_SET), 500); 110 | } 111 | return; 112 | } 113 | } 114 | 115 | /** 116 | * 实例化UpdateDialog 117 | * 118 | * @param context 119 | * @return 120 | */ 121 | public static UpdateDialog newInstance(Context context) { 122 | if (updateDialog == null) { 123 | synchronized (UpdateDialog.class) { 124 | if (updateDialog == null) { 125 | updateDialog = new UpdateDialog(); 126 | } 127 | } 128 | } 129 | UpdateDialog.context = context; 130 | return updateDialog; 131 | } 132 | 133 | 134 | public UpdateDialog setUpdateForce(boolean isForce) { 135 | this.isForce = isForce; 136 | return updateDialog; 137 | } 138 | 139 | public UpdateDialog setConfirmBgColor(int confirmBgColor) { 140 | this.confirmBgColor = confirmBgColor; 141 | return updateDialog; 142 | } 143 | 144 | public UpdateDialog setCancelBgColor(int cancelBgColor) { 145 | this.cancelBgColor = cancelBgColor; 146 | return updateDialog; 147 | } 148 | 149 | public UpdateDialog setConfirmBgResource(int confirmBgResource) { 150 | this.confirmBgResource = confirmBgResource; 151 | return updateDialog; 152 | } 153 | 154 | public UpdateDialog setCancelBgResource(int cancelBgResource) { 155 | this.cancelBgResource = cancelBgResource; 156 | return updateDialog; 157 | } 158 | 159 | public UpdateDialog setProgressDrawable(int progressDrawable) { 160 | this.progressDrawable = progressDrawable; 161 | return updateDialog; 162 | } 163 | 164 | public UpdateDialog setTopResId(int topResId) { 165 | this.topResId = topResId; 166 | return updateDialog; 167 | } 168 | 169 | public UpdateDialog setUpdateContent(String[] content) { 170 | this.content = content; 171 | return updateDialog; 172 | } 173 | 174 | public UpdateDialog setNewVernName(String newVerName) { 175 | this.newVerName = newVerName; 176 | return updateDialog; 177 | } 178 | 179 | public UpdateDialog setApkUrl(String apkUrl) { 180 | this.apkUrl = apkUrl; 181 | return updateDialog; 182 | } 183 | 184 | public UpdateDialog setConfirmText(String confirmText) { 185 | this.confirmText = confirmText; 186 | return updateDialog; 187 | } 188 | 189 | public UpdateDialog setTitle(String title) { 190 | this.title = title; 191 | return updateDialog; 192 | } 193 | 194 | public UpdateDialog setCancelText(String cancelText) { 195 | this.cancleText = cancelText; 196 | return updateDialog; 197 | } 198 | 199 | private void initView(View view) { 200 | ivTop = view.findViewById(R.id.ivTop); 201 | tvConfirm = view.findViewById(R.id.tvConfirm); 202 | tvConfirm.setOnClickListener(this); 203 | tvCancle = view.findViewById(R.id.tvCancle); 204 | tvCancle.setOnClickListener(this); 205 | tvTitle = view.findViewById(R.id.tvTitle); 206 | tvNewVerName = view.findViewById(R.id.tvNewVersionName); 207 | groupProgress = view.findViewById(R.id.groupProgress); 208 | recyclerView = view.findViewById(R.id.recyclerView); 209 | progressBar = view.findViewById(R.id.progressBar); 210 | tvProgress = view.findViewById(R.id.tvProgress); 211 | tvDownloadStatus = view.findViewById(R.id.tvDownloadStatus); 212 | layoutContent = view.findViewById(R.id.layoutContent); 213 | 214 | /** 215 | * 根据外部设置,初始化Dialog 216 | */ 217 | if (confirmBgColor != 0) { 218 | tvConfirm.setBackgroundColor(confirmBgColor); 219 | } 220 | if (cancelBgColor != 0) { 221 | tvCancle.setBackgroundColor(cancelBgColor); 222 | } 223 | if (confirmBgResource != 0) { 224 | tvConfirm.setBackgroundResource(confirmBgResource); 225 | } 226 | if (cancelBgResource != 0) { 227 | tvCancle.setBackgroundResource(cancelBgResource); 228 | } 229 | if (progressDrawable != 0) { 230 | progressBar.setProgressDrawable(context.getResources().getDrawable(progressDrawable)); 231 | } 232 | if (topResId != 0) 233 | ivTop.setImageResource(topResId); 234 | if (isForce) { 235 | tvCancle.setVisibility(View.GONE); 236 | } else { 237 | tvCancle.setVisibility(View.VISIBLE); 238 | } 239 | if (!TextUtils.isEmpty(newVerName)) 240 | tvNewVerName.setText(newVerName); 241 | if (!TextUtils.isEmpty(title)) 242 | tvTitle.setText(title); 243 | if (!TextUtils.isEmpty(confirmText)) 244 | tvConfirm.setText(confirmText); 245 | if (!TextUtils.isEmpty(cancleText)) 246 | tvCancle.setText(cancleText); 247 | 248 | List contentList = new ArrayList<>(); 249 | 250 | for (String str : content) { 251 | contentList.add(str); 252 | } 253 | 254 | adapter = new CommonRecycleViewAdapter(context, R.layout.appupdate_listitem_update_content, contentList) { 255 | @Override 256 | public void convert(ViewHolder holder, String content, int position) { 257 | ((TextView) holder.getView(R.id.tv_content)).setText(content); 258 | } 259 | }; 260 | recyclerView.setAdapter(adapter); 261 | recyclerView.setLayoutManager(new LinearLayoutManager(context)); 262 | } 263 | 264 | 265 | /** 266 | * 弹窗,请求跳转到设置页面开启权限 267 | */ 268 | private void requestPermissionSet() { 269 | AlertDialog.Builder builder = new AlertDialog.Builder(context); 270 | builder.setTitle(getString(R.string.appupdate_tip)) 271 | .setMessage(getString(R.string.appupdate_no_write_permission)) 272 | .setNegativeButton(getString(R.string.appupdate_cancel), (dialog, which) -> dialog.dismiss()) 273 | .setPositiveButton(getString(R.string.appupdate_open_permission), (dialog, which) -> toPermissionSettingPage()) 274 | .create().show(); 275 | } 276 | 277 | /** 278 | * 跳转权限设置页面 279 | */ 280 | private void toPermissionSettingPage() { 281 | Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 282 | Uri uri = Uri.fromParts("package", context.getPackageName(), null); 283 | intent.setData(uri); 284 | try { 285 | context.startActivity(intent); 286 | } catch (Exception e) { 287 | e.printStackTrace(); 288 | } 289 | } 290 | 291 | /** 292 | * “立即更新” 293 | */ 294 | private void update() { 295 | if (!TextUtils.isEmpty(apkUrl)) { 296 | 297 | String fileName = ApkUtil.getApkName(apkUrl); 298 | String directory = ApkUtil.getApkFileDir(context); 299 | 300 | Kalle.Download.get(apkUrl) 301 | .directory(directory) 302 | .fileName(fileName) 303 | .onProgress((progress, byteCount, speed) -> { 304 | tvProgress.setText(progress + "%"); 305 | progressBar.setProgress(progress); 306 | }) 307 | .perform(new Callback() { 308 | @Override 309 | public void onStart() { 310 | handler.sendEmptyMessage(MSG_WHAT_DOWNLOAD_START); 311 | } 312 | 313 | @Override 314 | public void onFinish(String path) { 315 | tvConfirm.setVisibility(View.VISIBLE); 316 | if (!isForce) 317 | tvCancle.setVisibility(View.VISIBLE); 318 | groupProgress.setVisibility(View.GONE); 319 | tvProgress.setText("0%"); 320 | progressBar.setProgress(0); 321 | ApkUtil.installApp(context, path); 322 | // if (!isForce) 323 | dismiss(); 324 | } 325 | 326 | @Override 327 | public void onException(Exception e) { 328 | tvConfirm.setVisibility(View.VISIBLE); 329 | if (!isForce) 330 | tvCancle.setVisibility(View.VISIBLE); 331 | groupProgress.setVisibility(View.GONE); 332 | tvProgress.setText("0%"); 333 | progressBar.setProgress(0); 334 | // TODO 模糊化 335 | tvDownloadStatus.setBackground(new BitmapDrawable(getResources(), ImageUtil.blur(context, ImageUtil.screenShotView(recyclerView), 336 | layoutContent.getWidth(), layoutContent.getHeight()))); 337 | tvDownloadStatus.setVisibility(View.VISIBLE); 338 | } 339 | 340 | @Override 341 | public void onCancel() { 342 | dismiss(); 343 | } 344 | 345 | @Override 346 | public void onEnd() { 347 | 348 | } 349 | }); 350 | } 351 | } 352 | 353 | /** 354 | * 更新progress、请求权限 355 | */ 356 | private WeakHandler handler = new WeakHandler(new Handler.Callback() { 357 | @Override 358 | public boolean handleMessage(Message msg) { 359 | if (msg.what == MSG_WHAT_PROGRESS) { 360 | Bundle bundle = msg.getData(); 361 | if (bundle != null) { 362 | int progress = bundle.getInt("progress"); 363 | if (progress > progressBar.getProgress()) { 364 | progressBar.setProgress(progress); 365 | tvProgress.setText(progress + "%"); 366 | } 367 | } 368 | } else if (msg.what == MSG_WHAT_REQUEST_PERMISSION_SET) { 369 | requestPermissionSet(); 370 | } else if (msg.what == MSG_WHAT_DOWNLOAD_START) { 371 | tvConfirm.setVisibility(View.GONE); 372 | tvCancle.setVisibility(View.GONE); 373 | groupProgress.setVisibility(View.VISIBLE); 374 | } 375 | return true; 376 | } 377 | }); 378 | 379 | } 380 | -------------------------------------------------------------------------------- /appupdate/src/main/res/drawable/appupdate_corner20dp_b9b9b9.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /appupdate/src/main/res/drawable/appupdate_corner20dp_color_primary.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /appupdate/src/main/res/drawable/appupdate_corner20dp_ffffff.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /appupdate/src/main/res/drawable/appupdate_corner5dp_b9b9b9.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /appupdate/src/main/res/drawable/appupdate_corner5dp_color_primary.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /appupdate/src/main/res/drawable/appupdate_progress_bg.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /appupdate/src/main/res/layout/appupdate_dialogfrag_update.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 28 | 29 | 30 | 37 | 38 | 50 | 51 | 60 | 61 | 62 | 63 | 64 | 70 | 71 | 82 | 83 | 90 | 91 | 92 | 93 | 94 | 103 | 104 | 121 | 122 | 139 | 140 | 152 | 153 | 165 | 166 | 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /appupdate/src/main/res/layout/appupdate_listitem_update_content.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 18 | 19 | -------------------------------------------------------------------------------- /appupdate/src/main/res/mipmap-xhdpi/appupdate_bg_app_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/appupdate/src/main/res/mipmap-xhdpi/appupdate_bg_app_top.png -------------------------------------------------------------------------------- /appupdate/src/main/res/mipmap-xxhdpi/appupdate_bg_app_top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/appupdate/src/main/res/mipmap-xxhdpi/appupdate_bg_app_top.png -------------------------------------------------------------------------------- /appupdate/src/main/res/values-en/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | App update 3 | Update now 4 | Cancle 5 | Update content:  6 | New version 7 | Tip 8 | Confirm 9 | Open 10 | Cancel 11 | Sorry, denied permission will not be updated. Please open in Settings. 12 | Update error 13 | 14 | -------------------------------------------------------------------------------- /appupdate/src/main/res/values-zh/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 软件更新 3 | 发现新版本 4 | 立即更新 5 | 暂不更新 6 | 更新内容: 7 | 提示 8 | 确定 9 | 开启 10 | 取消 11 | 抱歉,拒绝权限将无法更新版本,请到设置中开启 12 | 更 新 出 错 13 | 14 | -------------------------------------------------------------------------------- /appupdate/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #0078c8 4 | #333333 5 | #ffffff 6 | #b9b9b9 7 | #ff0000 8 | 9 | -------------------------------------------------------------------------------- /appupdate/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 软件更新 3 | 发现新版本 4 | 立即更新 5 | 暂不更新 6 | 更新内容: 7 | 提示 8 | 确定 9 | 开启 10 | 取消 11 | 抱歉,拒绝权限将无法更新版本,请到设置中开启 12 | 更 新 出 错 13 | 14 | -------------------------------------------------------------------------------- /appupdate/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /appupdate/src/main/res/xml/appupdate_fileprovicer_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | 15 | 16 | 19 | 20 | 23 | 24 | 27 | 28 | 31 | 32 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | google() 5 | jcenter() 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.3' 10 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | jcenter() 21 | maven { url 'https://jitpack.io' } 22 | 23 | } 24 | } 25 | 26 | task clean(type: Delete) { 27 | delete rootProject.buildDir 28 | } 29 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | 6 | defaultConfig { 7 | minSdkVersion 19 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 12 | } 13 | 14 | 15 | signingConfigs { 16 | config { 17 | keyAlias 'DrumBeat' 18 | keyPassword 'drumbeat' 19 | storeFile file('../keystore/keystore_drumbeat.jks') 20 | storePassword 'drumbeat' 21 | } 22 | } 23 | 24 | buildTypes { 25 | release { 26 | signingConfig signingConfigs.config 27 | minifyEnabled false 28 | zipAlignEnabled false 29 | shrinkResources false //移出无用代码 30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 31 | } 32 | debug { 33 | signingConfig signingConfigs.config 34 | //debug模式不进行崩溃上报,加快构建速度 35 | ext.enableCrashlytics = false 36 | zipAlignEnabled true 37 | shrinkResources false //移出无用代码 38 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 39 | } 40 | } 41 | 42 | compileOptions { 43 | sourceCompatibility JavaVersion.VERSION_1_8 44 | targetCompatibility JavaVersion.VERSION_1_8 45 | } 46 | } 47 | 48 | dependencies { 49 | implementation fileTree(dir: 'libs', include: ['*.jar']) 50 | 51 | implementation "androidx.appcompat:appcompat:1.1.0" 52 | implementation "junit:junit:4.12" 53 | implementation "androidx.test:runner:1.2.0" 54 | implementation "androidx.test.espresso:espresso-core:3.2.0" 55 | implementation "androidx.constraintlayout:constraintlayout:1.1.3" 56 | 57 | api project(':appupdate') 58 | // api rootProject.ext.dependencies["appUpdate"] 59 | } 60 | -------------------------------------------------------------------------------- /example/file/appupdate_example.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/example/file/appupdate_example.apk -------------------------------------------------------------------------------- /example/file/update_dialog.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/example/file/update_dialog.jpg -------------------------------------------------------------------------------- /example/file/update_error.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/example/file/update_error.jpg -------------------------------------------------------------------------------- /example/file/updating.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/example/file/updating.jpg -------------------------------------------------------------------------------- /example/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /example/src/androidTest/java/com/hailong/appupdate/exmple/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate.exmple; 2 | 3 | import android.content.Context; 4 | import androidx.test.InstrumentationRegistry; 5 | import androidx.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.hailong.appupdate.exmple", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /example/src/main/java/com/hailong/appupdate/exmple/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate.exmple; 2 | 3 | import android.os.Bundle; 4 | 5 | import androidx.fragment.app.FragmentActivity; 6 | 7 | import com.hailong.appupdate.AppUpdateManager; 8 | 9 | public class MainActivity extends FragmentActivity { 10 | 11 | // private static String[] arrayContent = new String[]{"1、实现apkUrl形式的版本更新功能", "2、实现stream形式的版本更新功能", "3、优化用户体验", "4、修复一些bug"}; 12 | private static String[] arrayContent = new String[]{"1、实现apkUrl形式的版本更新功能", "2、实现stream形式的版本更新功能", "3、优化用户体验", "4、修复一些bug", "1、实现apkUrl形式的版本更新功能", "2、实现stream形式的版本更新功能", "3、优化用户体验", "4、修复一些bug", "1、实现apkUrl形式的版本更新功能", "2、实现stream形式的版本更新功能", "3、优化用户体验", "4、修复一些bug", "1、实现apkUrl形式的版本更新功能", "2、实现stream形式的版本更新功能", "3、优化用户体验", "4、修复一些bug"}; 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_main); 18 | updateByApkUrl(); 19 | } 20 | 21 | @Override 22 | protected void onResume() { 23 | super.onResume(); 24 | } 25 | 26 | /** 27 | * 以apkUrl的下载链接的形式更新版本 28 | */ 29 | private void updateByApkUrl() { 30 | AppUpdateManager.Builder builder = new AppUpdateManager.Builder(MainActivity.this); 31 | //TODO github上的文件下载极慢(甚至连接失败),测试时可以更换为自己服务器上的文件链接 32 | // builder.apkUrl("https://github.com/ZuoHailong/AppUpdate/blob/master/example/file/appupdate_example.apk") 33 | builder.apkUrl("https://drumbeat-update-app.oss-cn-hangzhou.aliyuncs.com/Centralizer/develop/AppManager.apk") 34 | // .newVerName("2.2.2") 35 | .updateForce(false) 36 | .updateContent(arrayContent) 37 | .build(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /example/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /example/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /example/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /example/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/example/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/example/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/example/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/example/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/example/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/example/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/example/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/example/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/example/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/example/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /example/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | AppUpdate 3 | 4 | -------------------------------------------------------------------------------- /example/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /example/src/test/java/com/hailong/appupdate/exmple/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.hailong.appupdate.exmple; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | android.enableJetifier=true 10 | android.useAndroidX=true 11 | org.gradle.jvmargs=-Xmx512m 12 | # When configured, Gradle will run in incubating parallel mode. 13 | # This option should only be used with decoupled projects. More details, visit 14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 15 | # org.gradle.parallel=true 16 | 17 | 18 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jan 15 17:02:18 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /keystore/keystore_drumbeat.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZuoHailong/AppUpdate/7acd4569248e87b12d40fcbe17a30b2d534cfcac/keystore/keystore_drumbeat.jks -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':example', ':appupdate' 2 | --------------------------------------------------------------------------------