├── .gitignore ├── .idea ├── codeStyles │ └── Project.xml ├── gradle.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── .travis.yml ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── resources ├── fir.im.png └── res.png ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── sunfusheng │ │ └── firupdater │ │ └── sample │ │ ├── AppsAdapter.java │ │ ├── DataSource.java │ │ └── MainActivity.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── drawable_list_item_selector.xml │ └── ic_launcher_background.xml │ ├── layout │ ├── activity_main.xml │ ├── divider_1px.xml │ ├── divider_20dp.xml │ ├── item_list_layout.xml │ └── layout_recycler_view.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 │ ├── attrs.xml │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── settings.gradle ├── signings ├── keystore.jks ├── keystore.jks.enc └── signing.properties └── updater ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src └── main ├── AndroidManifest.xml ├── java └── com │ └── sunfusheng │ ├── AppInfo.java │ ├── FirUpdater.java │ ├── UpdaterApi.java │ ├── UpdaterDialog.java │ ├── UpdaterDownloader.java │ ├── UpdaterNotification.java │ ├── UpdaterService.java │ ├── UpdaterUtil.java │ └── download │ ├── DownloadInterceptor.java │ ├── DownloadProgressObserver.java │ ├── DownloadProgressResponseBody.java │ ├── DownloadService.java │ └── IDownloadListener.java └── res └── xml └── file_provider_paths.xml /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: android 2 | jdk: oraclejdk8 3 | sudo: true 4 | 5 | android: 6 | components: 7 | - tools 8 | - platform-tools 9 | - tools 10 | - build-tools-28.0.3 11 | - android-28 12 | - sys-img-armeabi-v7a-android-28 13 | - extra-android-support 14 | - extra-android-m2repository 15 | - extra-google-m2repository 16 | - add-on 17 | - extra 18 | licenses: 19 | - android-sdk-preview-license-52d11cd2 20 | - android-sdk-license-.+ 21 | - google-gdk-license-.+ 22 | 23 | before_cache: 24 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 25 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 26 | cache: 27 | directories: 28 | - "$HOME/.gradle/caches/" 29 | - "$HOME/.gradle/wrapper/" 30 | - "$HOME/.android/build-cache" 31 | 32 | before_install: 33 | - mkdir "$ANDROID_HOME/licenses" || true 34 | - echo "d56f5187479451eabf01fb78af6dfcb131a6481e" > "$ANDROID_HOME/licenses/android-sdk-license" 35 | - openssl aes-256-cbc -K $encrypted_0895791d098d_key -iv $encrypted_0895791d098d_iv 36 | -in signings/keystore.jks.enc -out signings/keystore.jks -d 37 | - gem install fir-cli 38 | 39 | before_script: 40 | - chmod +x gradlew 41 | 42 | script: 43 | - "./gradlew assembleRelease" 44 | 45 | deploy: 46 | provider: releases 47 | api_key: 48 | secure: OFuj/KAKzHLJ09FZkUKggwpKtXPXTpbrpGaSDE0osd/KM6zDy9kl7/NJRp/RgWMeiRQnWZY98lJatIbjTQ8mZBFJ0bpKuNaWf9+Kd61oPuNDaUoG02ogXi/zZLRRQszDMNNSZB4ND4Avv9DO6Tx30VCPN5lH9L2pR94GXf8CU47dicBBrVkZvVlXOWVsUke8nLdBN57F58bkXmRf2OHaHtIFPGLCr9AoSKzQxHVMeAcQB3Wf7ti/eP/0s1DLaHqjUcT145gIkftrKT38xllV/CH9p3S7lbx4SCsMiDbgTJLnGNf4Y/uulYFUuhePUHPfPQPGOs/u7jvDm3H91uKWRQSXBONeW2/sc96QHC0u0/S2IpPqTIayIaCCzELCWUgizV1PaNfIUret2rNz7crPMMjgO+V3qtpvv1NsIjmal6sTVhddMmuRUNkaxI2ZnX9otXPwXP7wMkTkL1L6bkmBwNDtgQzLK7BNKMhG6jTnq40A5XIVGN+0uS/Q7VwDhRSPeuerNpIBPF7fFTLV1LjVJvUZdb0tAo6BGjIcO9dJ4+X7d6ACB0cqQfIN7KGycWFSW1TFaBUj2voUhnzadcMsxvS27tquKQ1PHX7aqfUMwuBIunXmJs8xElv36CZUN2xcjs6DKiQ/cNC2R+vjioMWRlLXp2Glh5t23qPU9qTjLXo= 49 | file: sample/release/FirUpdater.apk 50 | skip_cleanup: true 51 | on: 52 | repo: sunfusheng/FirUpdater 53 | tags: true 54 | all_branches: true 55 | 56 | after_deploy: 57 | - "./gradlew clean build bintrayUpload -PbintrayUser=sfsheng0322 -PbintrayKey=$BINTRAY_TOKEN -PdryRun=false" 58 | - fir publish sample/release/FirUpdater.apk -T $FIR_TOKEN -c "修复了一些问题,优化体验" 59 | 60 | notifications: 61 | email: 62 | - sfsheng0322@126.com 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FirUpdater [ ![Download](https://api.bintray.com/packages/sfsheng0322/maven/FirUpdater/images/download.svg) ](https://bintray.com/sfsheng0322/maven/FirUpdater/_latestVersion) 2 | 3 | Fir.im通道APK更新器,使用简单,让自己的demo快速具备升级功能 4 | 5 |
6 | 7 | ### 应用截图 8 | 9 | ![](resources/res.png) 10 | 11 |
12 | 13 | ### Gradle: 14 | 15 | 工程的 build.gradle 添加: 16 | 17 | allprojects { 18 | repositories { 19 | maven { url 'https://dl.bintray.com/sfsheng0322/maven' } 20 | } 21 | } 22 | 23 | 模块的 build.gradle 添加: 24 | 25 | compile 'com.sunfusheng:FirUpdater:' 26 | 27 | Android 8.0以上需要添加权限 28 | 29 | 30 | 31 |
32 | 33 | ### 使用 34 | 35 | 1、检查升级一行代码即可: 36 | 37 | FirUpdater.getInstance(context) 38 | .apiToken(YOUR_FIR_API_TOKEN) 39 | .appId(YOUR_FIR_APP_ID) 40 | .apkPath(YOUR_APK_PATH) 41 | .apkName(YOUR_APK_NAME) 42 | .checkVersion(); 43 | 44 | 2、如果不设置apkPath,默认下载到SDCard的根目录下 45 | 46 | 3、如果不设置apkName,默认名称为【appName-versionName】 47 | 48 |
49 | 50 | ### 扫一扫[Fir.im](https://fir.im/FirUpdater)二维码下载APK 51 | 52 | 53 | 54 |
55 | 56 | ### 个人微信公众号 57 | 58 | 59 | 60 |
61 | 62 | ### 请作者喝杯咖啡^_^ 63 | 64 | 65 | 66 |
67 | 68 | ### 关于我 69 | 70 | [GitHub: sunfusheng](https://github.com/sunfusheng) 71 | 72 | [个人邮箱: sfsheng0322@126.com](https://mail.126.com/) 73 | 74 | [个人博客: sunfusheng.com](http://sunfusheng.com/) 75 | 76 | [简书主页](http://www.jianshu.com/users/88509e7e2ed1/latest_articles) 77 | 78 | [新浪微博](http://weibo.com/u/3852192525) -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | compile_sdk_version = 29 4 | min_sdk_version = 19 5 | target_sdk_version = 29 6 | version_code = 14 7 | version_name = '1.3.0' 8 | 9 | okhttp3_version = '4.4.1' 10 | retrofit_version = '2.8.1' 11 | } 12 | 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | 18 | dependencies { 19 | classpath 'com.android.tools.build:gradle:3.6.2' 20 | classpath 'com.novoda:bintray-release:0.9.2' 21 | } 22 | } 23 | 24 | allprojects { 25 | repositories { 26 | google() 27 | jcenter() 28 | } 29 | } 30 | 31 | task clean(type: Delete) { 32 | delete rootProject.buildDir 33 | } 34 | -------------------------------------------------------------------------------- /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 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Aug 30 20:38:02 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.6.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /resources/fir.im.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/resources/fir.im.png -------------------------------------------------------------------------------- /resources/res.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/resources/res.png -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | Properties signingProperties = new Properties() 4 | signingProperties.load(new FileInputStream(file("../signings/signing.properties"))) 5 | 6 | android { 7 | compileSdkVersion compile_sdk_version 8 | defaultConfig { 9 | applicationId "com.sunfusheng.firupdater.sample" 10 | minSdkVersion min_sdk_version 11 | targetSdkVersion target_sdk_version 12 | versionCode version_code 13 | versionName version_name 14 | } 15 | 16 | compileOptions { 17 | sourceCompatibility JavaVersion.VERSION_1_8 18 | targetCompatibility JavaVersion.VERSION_1_8 19 | } 20 | 21 | signingConfigs { 22 | release { 23 | storeFile file(signingProperties['KEYSTORE_FILEPATH']) 24 | storePassword signingProperties['STORE_PASSWORD'] 25 | keyAlias signingProperties['KEY_ALIAS'] 26 | keyPassword signingProperties['KEY_PASSWORD'] 27 | } 28 | } 29 | 30 | buildTypes { 31 | debug { 32 | buildConfigField "boolean", "debugMode", "true" 33 | signingConfig signingConfigs.release 34 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 35 | } 36 | release { 37 | buildConfigField "boolean", "debugMode", "false" 38 | signingConfig signingConfigs.release 39 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 40 | applicationVariants.all { variant -> 41 | variant.outputs.all { 42 | outputFileName = "FirUpdater.apk" 43 | } 44 | } 45 | } 46 | } 47 | } 48 | 49 | dependencies { 50 | implementation fileTree(dir: 'libs', include: ['*.jar']) 51 | implementation 'androidx.appcompat:appcompat:1.1.0' 52 | implementation 'com.google.android.material:material:1.2.0-alpha05' 53 | implementation 'com.sunfusheng:GroupRecyclerViewAdapter:1.2.0' 54 | 55 | implementation project(':updater') 56 | } 57 | -------------------------------------------------------------------------------- /sample/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 | -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /sample/src/main/java/com/sunfusheng/firupdater/sample/AppsAdapter.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng.firupdater.sample; 2 | 3 | import android.content.Context; 4 | 5 | import com.sunfusheng.GroupViewHolder; 6 | import com.sunfusheng.HeaderGroupRecyclerViewAdapter; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * @author sunfusheng on 2018/2/17. 12 | */ 13 | public class AppsAdapter extends HeaderGroupRecyclerViewAdapter { 14 | 15 | public AppsAdapter(Context context) { 16 | super(context); 17 | } 18 | 19 | public AppsAdapter(Context context, List> items) { 20 | super(context, items); 21 | } 22 | 23 | public AppsAdapter(Context context, DataSource.AppConfig[][] items) { 24 | super(context, items); 25 | } 26 | 27 | @Override 28 | public int getHeaderLayoutId(int viewType) { 29 | return R.layout.divider_20dp; 30 | } 31 | 32 | @Override 33 | public int getChildLayoutId(int viewType) { 34 | return R.layout.item_list_layout; 35 | } 36 | 37 | @Override 38 | public void onBindHeaderViewHolder(GroupViewHolder holder, DataSource.AppConfig item, int groupPosition) { 39 | 40 | } 41 | 42 | @Override 43 | public void onBindChildViewHolder(GroupViewHolder holder, DataSource.AppConfig item, int groupPosition, int childPosition) { 44 | holder.setText(R.id.tv_title, item.titleId); 45 | holder.setText(R.id.tv_subtitle, item.subtitleId); 46 | holder.setVisible(R.id.divider, !isGroupLastItem(groupPosition, childPosition)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /sample/src/main/java/com/sunfusheng/firupdater/sample/DataSource.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng.firupdater.sample; 2 | 3 | /** 4 | * @author sunfusheng on 2018/2/17. 5 | */ 6 | public class DataSource { 7 | 8 | public static final String API_TOKEN = "3c57fb226edf7facf821501e4eba08d2"; 9 | public static final String FIR_UPDATER_APP_ID = "5a89af10548b7a760110c918"; 10 | 11 | public static AppConfig[][] apps = { 12 | {AppConfig.NULL, AppConfig.MultiType, AppConfig.MV, AppConfig.GIV, AppConfig.GRVA}, 13 | {AppConfig.NULL, AppConfig.GANK, AppConfig.SHLV, AppConfig._360, AppConfig.TULING} 14 | }; 15 | 16 | public enum AppConfig { 17 | NULL(0, 0, 0, 0, 0), 18 | 19 | MultiType(R.attr.key_multitype, R.string.multitype_title, R.string.multitype_subtitle, R.string.multitype_app_id, R.string.multitype_pkg), 20 | MV(R.attr.key_mv, R.string.mv_title, R.string.mv_subtitle, R.string.mv_app_id, R.string.mv_pkg), 21 | GIV(R.attr.key_giv, R.string.giv_title, R.string.giv_subtitle, R.string.giv_app_id, R.string.giv_pkg), 22 | GRVA(R.attr.key_grva, R.string.grva_title, R.string.grva_subtitle, R.string.grva_app_id, R.string.grva_pkg), 23 | 24 | GANK(R.attr.key_gank, R.string.gank_title, R.string.gank_subtitle, R.string.gank_app_id, R.string.gank_pkg), 25 | SHLV(R.attr.key_shlv, R.string.shlv_title, R.string.shlv_subtitle, R.string.shlv_app_id, R.string.shlv_pkg), 26 | _360(R.attr.key_360, R.string._360_title, R.string._360_subtitle, R.string._360_app_id, R.string._360_pkg), 27 | TULING(R.attr.key_tuling, R.string.tuling_title, R.string.tuling_subtitle, R.string.tuling_app_id, R.string.tuling_pkg); 28 | 29 | public int key; 30 | public int titleId; 31 | public int subtitleId; 32 | public int appId; 33 | public int pkgId; 34 | 35 | AppConfig(int key, int titleId, int subtitleId, int appId, int pkgId) { 36 | this.key = key; 37 | this.titleId = titleId; 38 | this.subtitleId = subtitleId; 39 | this.appId = appId; 40 | this.pkgId = pkgId; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sample/src/main/java/com/sunfusheng/firupdater/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng.firupdater.sample; 2 | 3 | import android.os.Bundle; 4 | 5 | import androidx.appcompat.app.AppCompatActivity; 6 | import androidx.recyclerview.widget.LinearLayoutManager; 7 | import androidx.recyclerview.widget.RecyclerView; 8 | 9 | import com.sunfusheng.FirUpdater; 10 | import com.sunfusheng.UpdaterUtil; 11 | 12 | public class MainActivity extends AppCompatActivity { 13 | 14 | @Override 15 | protected void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.layout_recycler_view); 18 | 19 | setTitle(getString(R.string.app_name) + "(V" + UpdaterUtil.getVersionName(this) + ")"); 20 | 21 | FirUpdater.getInstance(this) 22 | .apiToken(DataSource.API_TOKEN) 23 | .appId(DataSource.FIR_UPDATER_APP_ID) 24 | .packageName(UpdaterUtil.getPackageName(this)) 25 | .checkVersion(); 26 | 27 | RecyclerView recyclerView = findViewById(R.id.recyclerView); 28 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 29 | AppsAdapter appsAdapter = new AppsAdapter(this, DataSource.apps); 30 | recyclerView.setAdapter(appsAdapter); 31 | 32 | appsAdapter.setOnItemClickListener((adapter, holder, groupPosition, childPosition) -> { 33 | DataSource.AppConfig config = appsAdapter.getItem(groupPosition, childPosition); 34 | if (config.key == 0) { 35 | return; 36 | } 37 | 38 | if (UpdaterUtil.isAppInstalled(this, getString(config.pkgId))) { 39 | UpdaterUtil.startApp(this, getString(config.pkgId)); 40 | return; 41 | } 42 | 43 | FirUpdater.getInstance(this) 44 | .apiToken(DataSource.API_TOKEN) 45 | .appId(getString(config.appId)) 46 | .packageName(getString(config.pkgId)) 47 | .checkVersion(); 48 | }); 49 | 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/drawable_list_item_selector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 16 | 21 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 76 | 81 | 86 | 91 | 96 | 101 | 106 | 111 | 116 | 121 | 126 | 131 | 136 | 141 | 146 | 151 | 156 | 161 | 166 | 171 | 172 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/divider_1px.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/divider_20dp.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/item_list_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 16 | 17 | 26 | 27 | 37 | 38 | 39 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/layout_recycler_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | key_multitype 5 | key_grva 6 | key_360 7 | key_giv 8 | key_gank 9 | key_mv 10 | key_shlv 11 | key_tuling 12 | 13 | -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #373a41 5 | #373a41 6 | #373a41 7 | 8 | #ffffffff 9 | #ff000000 10 | #ffa500 11 | #1aad19 12 | 13 | #333333 14 | #9d9d9d 15 | 16 | #ebebeb 17 | #d6d6d6 18 | 19 | #ebebeb 20 | #d6d6d6 21 | 22 | #9e9e9e 23 | #bdbdbd 24 | #e0e0e0 25 | 26 | 27 | -------------------------------------------------------------------------------- /sample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 14dp 5 | 12dp 6 | 10dp 7 | 8 | 9 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | FirUpdater 4 | 5 | 6 | GroupRecyclerViewAdapter 7 | 可增删改查、可展开收起的分组列表 8 | 5a80151e959d6949f2ecb2e8 9 | com.sunfusheng.adapter.sample 10 | 11 | 12 | DroidVR 13 | 360°全景图是一个值得把玩的应用 14 | 59a52a7d959d69410b00060d 15 | com.sunfusheng.vr 16 | 17 | 18 | GlideImageView 19 | 基于Glide V4.X封装的可显示加载网络图片进度的图片库 20 | 59461f4c548b7a45cb000050 21 | com.sunfusheng.glideimageview.sample 22 | 23 | 24 | Gank.IO 25 | 干货集中营的Android客户端 26 | 58954512ca87a85dc4000135 27 | com.sunfusheng.gank 28 | 29 | 30 | MarqueeView 31 | 可垂直翻、可水平翻的翻页公告 32 | 574e8c15f2fc427ab1000009 33 | com.sunfusheng.sample 34 | 35 | 36 | StickyHeaderListView 37 | 可渐变、可悬浮、可筛选、可排序灵活可定制的ListView 38 | 571dd00b00fc74312e00001f 39 | com.sunfusheng.StickyHeaderListView 40 | 41 | 42 | Tuling 43 | 图灵机器人,一起和它玩耍吧 44 | 5704953c00fc74127000000a 45 | com.robot.tuling 46 | 47 | MultiType 48 | 轻松地实现RecyclerView显示多种类型数据,一对多、多对多,可以注册全局类型、局部类型、默认类型 49 | 5b3ec988548b7a3d7bd77c8f 50 | com.sunfusheng.multitype.sample 51 | 52 | 53 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':updater', 2 | ':sample' 3 | -------------------------------------------------------------------------------- /signings/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/signings/keystore.jks -------------------------------------------------------------------------------- /signings/keystore.jks.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunfusheng/FirUpdater/03b9ccdba67f3e2f30289456311acf80e89a66de/signings/keystore.jks.enc -------------------------------------------------------------------------------- /signings/signing.properties: -------------------------------------------------------------------------------- 1 | KEYSTORE_FILEPATH=../signings/keystore.jks 2 | STORE_PASSWORD=sunfusheng 3 | KEY_ALIAS=sunfusheng 4 | KEY_PASSWORD=sunfusheng -------------------------------------------------------------------------------- /updater/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /updater/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'com.novoda.bintray-release' 3 | 4 | android { 5 | compileSdkVersion compile_sdk_version 6 | defaultConfig { 7 | minSdkVersion min_sdk_version 8 | targetSdkVersion target_sdk_version 9 | versionCode version_code 10 | versionName version_name 11 | } 12 | 13 | compileOptions { 14 | sourceCompatibility JavaVersion.VERSION_1_8 15 | targetCompatibility JavaVersion.VERSION_1_8 16 | } 17 | 18 | lintOptions { 19 | abortOnError false 20 | } 21 | 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | } 29 | 30 | dependencies { 31 | implementation 'androidx.appcompat:appcompat:1.1.0' 32 | implementation 'com.squareup.okio:okio:2.4.3' 33 | implementation 'com.google.code.gson:gson:2.8.6' 34 | implementation "com.squareup.okhttp3:okhttp:$okhttp3_version" 35 | implementation "com.squareup.okhttp3:logging-interceptor:$okhttp3_version" 36 | implementation "com.squareup.retrofit2:retrofit:$retrofit_version" 37 | implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" 38 | implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit_version" 39 | implementation 'io.reactivex.rxjava2:rxjava:2.2.13' 40 | implementation 'io.reactivex.rxjava2:rxandroid:2.1.1' 41 | implementation 'com.qw:soulpermission:1.2.1' 42 | } 43 | 44 | publish { 45 | userOrg = 'sfsheng0322' 46 | groupId = 'com.sunfusheng' 47 | artifactId = 'FirUpdater' 48 | publishVersion = version_name 49 | desc = 'A updater of the fir.im' 50 | website = 'https://github.com/sunfusheng/FirUpdater' 51 | } 52 | -------------------------------------------------------------------------------- /updater/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 | -------------------------------------------------------------------------------- /updater/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /updater/src/main/java/com/sunfusheng/AppInfo.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng; 2 | 3 | /** 4 | * @author by sunfusheng on 2019-08-13 5 | */ 6 | class AppInfo { 7 | public String name; 8 | public String version; 9 | public String changelog; 10 | public int updated_at; 11 | public String versionShort; 12 | public String build; 13 | public String installUrl; 14 | public String install_url; 15 | public String direct_install_url; 16 | public String update_url; 17 | public BinaryBean binary; 18 | 19 | public static class BinaryBean { 20 | public int fsize; 21 | 22 | @Override 23 | public String toString() { 24 | return "BinaryBean{" + 25 | "fsize=" + fsize + 26 | '}'; 27 | } 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return "AppInfo{\n" + 33 | "appName=【" + name + "】\n" + 34 | "versionCode=" + version + "\n" + 35 | "versionName=" + versionShort + "\n" + 36 | "size=" + UpdaterUtil.getFileSizeDesc(binary.fsize) + "\n" + 37 | "changeLog=" + changelog + "\n" + 38 | "updateUrl=" + update_url + "\n" + 39 | "installUrl=" + install_url + "\n" + 40 | "}"; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /updater/src/main/java/com/sunfusheng/FirUpdater.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng; 2 | 3 | import android.content.Context; 4 | import android.os.Environment; 5 | import android.text.TextUtils; 6 | import android.util.Log; 7 | import android.widget.Toast; 8 | 9 | import com.sunfusheng.download.IDownloadListener; 10 | 11 | import java.io.File; 12 | 13 | import io.reactivex.android.schedulers.AndroidSchedulers; 14 | import io.reactivex.disposables.Disposable; 15 | import io.reactivex.schedulers.Schedulers; 16 | 17 | /** 18 | * @author by sunfusheng on 2019-08-07 19 | */ 20 | public class FirUpdater { 21 | private static final String TAG = FirUpdater.class.getCanonicalName(); 22 | private volatile static FirUpdater mInstance; 23 | private Context mContext; 24 | private String mApiToken; 25 | private String mAppId; 26 | private String mApkPath; 27 | private String mApkName; 28 | private String mPackageName; 29 | 30 | private AppInfo mAppInfo; 31 | private UpdaterDialog mDialog; 32 | private UpdaterDownloader mDownloader; 33 | private int mCurrProgress; 34 | private UpdaterNotification mNotification; 35 | 36 | public static FirUpdater getInstance(Context context) { 37 | if (mInstance == null) { 38 | synchronized (FirUpdater.class) { 39 | if (mInstance == null) { 40 | mInstance = new FirUpdater(context); 41 | } 42 | } 43 | } 44 | return mInstance; 45 | } 46 | 47 | private FirUpdater(Context context) { 48 | this.mContext = context.getApplicationContext(); 49 | } 50 | 51 | public FirUpdater apiToken(String apiToken) { 52 | this.mApiToken = apiToken; 53 | return this; 54 | } 55 | 56 | public FirUpdater appId(String appId) { 57 | this.mAppId = appId; 58 | return this; 59 | } 60 | 61 | public FirUpdater apkPath(String apkPath) { 62 | this.mApkPath = apkPath; 63 | return this; 64 | } 65 | 66 | public FirUpdater apkName(String apkName) { 67 | this.mApkName = apkName; 68 | return this; 69 | } 70 | 71 | @Deprecated 72 | public FirUpdater packageName(String packageName) { 73 | this.mPackageName = packageName; 74 | return this; 75 | } 76 | 77 | public void checkVersion() { 78 | if (TextUtils.isEmpty(mApiToken) || TextUtils.isEmpty(mAppId)) { 79 | Toast.makeText(mContext, "请设置 ApiToken 和 AppId.", Toast.LENGTH_SHORT).show(); 80 | return; 81 | } 82 | 83 | Disposable disposable = UpdaterApi.getUpdaterService().fetchAppInfo(mAppId, mApiToken) 84 | .subscribeOn(Schedulers.io()) 85 | .filter(it -> it != null && !TextUtils.isEmpty(it.installUrl)) 86 | .filter(it -> it.binary != null && it.binary.fsize > 0) 87 | .observeOn(AndroidSchedulers.mainThread()) 88 | .subscribe(it -> { 89 | mAppInfo = it; 90 | Log.d(TAG, it.toString()); 91 | int remoteVersionCode = !TextUtils.isEmpty(it.version) ? Integer.parseInt(it.version) : -1; 92 | int localVersionCode = UpdaterUtil.getVersionCode(mContext, mPackageName); 93 | if (remoteVersionCode <= localVersionCode) { 94 | Log.d(TAG, "当前已是最新版本"); 95 | return; 96 | } 97 | 98 | mDialog = new UpdaterDialog(); 99 | mDialog.showUpdateDialog(it); 100 | mDialog.setOnClickDownloadDialogListener(new UpdaterDialog.OnClickDownloadDialogListener() { 101 | @Override 102 | public void onClickDownload() { 103 | download(); 104 | } 105 | 106 | @Override 107 | public void onClickBackground() { 108 | mDialog.dismissDownloadDialog(); 109 | mNotification = new UpdaterNotification(mContext, mAppInfo.name); 110 | mNotification.setProgress(mCurrProgress); 111 | } 112 | 113 | @Override 114 | public void onClickCancel() { 115 | mDialog.dismissDownloadDialog(); 116 | mDownloader.cancel(); 117 | } 118 | }); 119 | }, Throwable::printStackTrace); 120 | } 121 | 122 | private void download() { 123 | if (TextUtils.isEmpty(mApkName)) { 124 | mApkName = mAppInfo.name + "-V" + mAppInfo.versionShort + ".apk"; 125 | } 126 | if (TextUtils.isEmpty(mApkPath)) { 127 | mApkPath = Environment.getExternalStorageDirectory() + File.separator; 128 | } 129 | String apkPathName = mApkPath + mApkName; 130 | 131 | mDialog.showDownloadDialog(); 132 | mDownloader = new UpdaterDownloader(); 133 | mDownloader.download(mAppInfo.install_url, apkPathName, new IDownloadListener() { 134 | @Override 135 | public void onStart() { 136 | } 137 | 138 | @Override 139 | public void onProgress(long bytesTransferred, long totalBytes, int percentage) { 140 | mCurrProgress = percentage; 141 | mDialog.setDownloadProgress(percentage); 142 | if (mNotification != null) { 143 | mNotification.setProgress(percentage); 144 | } 145 | } 146 | 147 | @Override 148 | public void onSuccess(File file) { 149 | mDialog.dismissDownloadDialog(); 150 | if (mNotification != null) { 151 | mNotification.cancel(); 152 | } 153 | UpdaterUtil.installApk(mContext, apkPathName); 154 | } 155 | 156 | @Override 157 | public void onError(Throwable e) { 158 | mDialog.dismissDownloadDialog(); 159 | if (mNotification != null) { 160 | mNotification.cancel(); 161 | } 162 | Toast.makeText(mContext, "下载失败,请稍后重试!", Toast.LENGTH_SHORT).show(); 163 | } 164 | }); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /updater/src/main/java/com/sunfusheng/UpdaterApi.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng; 2 | 3 | import com.sunfusheng.download.DownloadInterceptor; 4 | import com.sunfusheng.download.DownloadService; 5 | import com.sunfusheng.download.IDownloadListener; 6 | 7 | import java.util.concurrent.TimeUnit; 8 | 9 | import okhttp3.Interceptor; 10 | import okhttp3.OkHttpClient; 11 | import okhttp3.logging.HttpLoggingInterceptor; 12 | import retrofit2.Retrofit; 13 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; 14 | import retrofit2.converter.gson.GsonConverterFactory; 15 | 16 | /** 17 | * @author by sunfusheng on 2019-08-13 18 | */ 19 | class UpdaterApi { 20 | private static Retrofit mRetrofit; 21 | 22 | private static OkHttpClient newOkHttpClient(Interceptor... interceptors) { 23 | OkHttpClient.Builder builder = new OkHttpClient.Builder() 24 | .connectTimeout(60, TimeUnit.SECONDS) 25 | .readTimeout(60, TimeUnit.SECONDS) 26 | .writeTimeout(60, TimeUnit.SECONDS) 27 | .addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BASIC)); 28 | 29 | if (interceptors != null && interceptors.length > 0) { 30 | for (Interceptor interceptor : interceptors) { 31 | builder.addInterceptor(interceptor); 32 | } 33 | } 34 | return builder.build(); 35 | } 36 | 37 | private static Retrofit newRetrofit(OkHttpClient okHttpClient) { 38 | return new Retrofit.Builder() 39 | .client(okHttpClient) 40 | .baseUrl(UpdaterService.BASE_URL) 41 | .addConverterFactory(GsonConverterFactory.create()) 42 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 43 | .build(); 44 | } 45 | 46 | static UpdaterService getUpdaterService() { 47 | if (mRetrofit == null) { 48 | mRetrofit = newRetrofit(newOkHttpClient()); 49 | } 50 | return mRetrofit.create(UpdaterService.class); 51 | } 52 | 53 | static DownloadService getDownloadService(IDownloadListener downloadListener) { 54 | return newRetrofit(newOkHttpClient(new DownloadInterceptor(downloadListener))).create(DownloadService.class); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /updater/src/main/java/com/sunfusheng/UpdaterDialog.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng; 2 | 3 | import android.app.Activity; 4 | import android.app.ProgressDialog; 5 | import android.content.DialogInterface; 6 | import android.graphics.Color; 7 | import android.text.TextUtils; 8 | import android.util.Log; 9 | import android.widget.TextView; 10 | 11 | import androidx.appcompat.app.AlertDialog; 12 | 13 | import com.qw.soul.permission.SoulPermission; 14 | 15 | import java.lang.reflect.Field; 16 | 17 | /** 18 | * @author sunfusheng 19 | * @since 2019-09-11 20 | */ 21 | class UpdaterDialog { 22 | private ProgressDialog mProgressDialog; 23 | private OnClickDownloadDialogListener onClickDownloadDialogListener; 24 | 25 | void showUpdateDialog(AppInfo appInfo) { 26 | Activity topActivity = SoulPermission.getInstance().getTopActivity(); 27 | if (topActivity == null) { 28 | Log.e("FirUpdater", "topActivity = null"); 29 | return; 30 | } 31 | 32 | StringBuilder msg = new StringBuilder(); 33 | msg.append("名称:").append(appInfo.name); 34 | msg.append("\n版本:V").append(appInfo.versionShort); 35 | msg.append("\n大小:").append(UpdaterUtil.getFileSizeDesc(appInfo.binary.fsize)); 36 | if (!TextUtils.isEmpty(appInfo.changelog)) { 37 | msg.append("\n\n更新日志:").append(appInfo.changelog); 38 | } 39 | 40 | AlertDialog alertDialog = new AlertDialog.Builder(topActivity) 41 | .setCancelable(false) 42 | .setTitle("应用更新提示") 43 | .setMessage(msg) 44 | .setPositiveButton("立即更新", (dialog, which) -> { 45 | if (onClickDownloadDialogListener != null) { 46 | onClickDownloadDialogListener.onClickDownload(); 47 | } 48 | }) 49 | .setNegativeButton("稍后", (dialog, which) -> { 50 | }) 51 | .create(); 52 | alertDialog.show(); 53 | 54 | try { 55 | alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(Color.parseColor("#333333")); 56 | alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(Color.parseColor("#9a9a9a")); 57 | 58 | Field alert = AlertDialog.class.getDeclaredField("mAlert"); 59 | alert.setAccessible(true); 60 | Object alertController = alert.get(alertDialog); 61 | Field messageView = alertController.getClass().getDeclaredField("mMessageView"); 62 | messageView.setAccessible(true); 63 | TextView textView = (TextView) messageView.get(alertController); 64 | textView.setTextSize(13); 65 | textView.setTextColor(Color.parseColor("#9a9a9a")); 66 | } catch (Exception e) { 67 | e.printStackTrace(); 68 | } 69 | } 70 | 71 | void showDownloadDialog() { 72 | Activity topActivity = SoulPermission.getInstance().getTopActivity(); 73 | if (topActivity == null) { 74 | Log.e("FirUpdater", "topActivity = null"); 75 | return; 76 | } 77 | 78 | if (mProgressDialog == null) { 79 | mProgressDialog = new ProgressDialog(topActivity); 80 | mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); 81 | mProgressDialog.setCancelable(false); 82 | mProgressDialog.setCanceledOnTouchOutside(false); 83 | mProgressDialog.setMax(100); 84 | mProgressDialog.setTitle("正在下载"); 85 | mProgressDialog.setButton(DialogInterface.BUTTON_POSITIVE, "后台下载", (dialog, which) -> { 86 | if (onClickDownloadDialogListener != null) { 87 | onClickDownloadDialogListener.onClickBackground(); 88 | } 89 | }); 90 | mProgressDialog.setButton(DialogInterface.BUTTON_NEGATIVE, "取消", (dialog, which) -> { 91 | if (onClickDownloadDialogListener != null) { 92 | onClickDownloadDialogListener.onClickCancel(); 93 | } 94 | }); 95 | 96 | } 97 | 98 | if (!mProgressDialog.isShowing()) { 99 | mProgressDialog.setProgress(0); 100 | mProgressDialog.show(); 101 | } 102 | 103 | mProgressDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(Color.parseColor("#333333")); 104 | mProgressDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(Color.parseColor("#9a9a9a")); 105 | } 106 | 107 | void setDownloadProgress(int progress) { 108 | if (mProgressDialog != null && mProgressDialog.isShowing()) { 109 | mProgressDialog.setProgress(progress); 110 | } 111 | } 112 | 113 | void dismissDownloadDialog() { 114 | if (mProgressDialog != null && mProgressDialog.isShowing()) { 115 | mProgressDialog.dismiss(); 116 | } 117 | } 118 | 119 | void setOnClickDownloadDialogListener(OnClickDownloadDialogListener listener) { 120 | this.onClickDownloadDialogListener = listener; 121 | } 122 | 123 | interface OnClickDownloadDialogListener { 124 | void onClickDownload(); 125 | 126 | void onClickBackground(); 127 | 128 | void onClickCancel(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /updater/src/main/java/com/sunfusheng/UpdaterDownloader.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng; 2 | 3 | import android.Manifest; 4 | 5 | import com.qw.soul.permission.SoulPermission; 6 | import com.qw.soul.permission.bean.Permission; 7 | import com.qw.soul.permission.bean.Permissions; 8 | import com.qw.soul.permission.callbcak.CheckRequestPermissionsListener; 9 | import com.sunfusheng.download.DownloadProgressObserver; 10 | import com.sunfusheng.download.IDownloadListener; 11 | 12 | import java.io.File; 13 | import java.io.FileOutputStream; 14 | import java.io.InputStream; 15 | 16 | import io.reactivex.android.schedulers.AndroidSchedulers; 17 | import io.reactivex.schedulers.Schedulers; 18 | import okhttp3.ResponseBody; 19 | 20 | /** 21 | * @author sunfusheng 22 | * @since 2019-09-16 23 | */ 24 | class UpdaterDownloader { 25 | private static final Permissions permissions = Permissions.build( 26 | Manifest.permission.READ_EXTERNAL_STORAGE, 27 | Manifest.permission.WRITE_EXTERNAL_STORAGE 28 | ); 29 | 30 | private DownloadProgressObserver mDownloadProgressObserver; 31 | 32 | void download(String url, String filePathName, IDownloadListener downloadListener) { 33 | SoulPermission.getInstance().checkAndRequestPermissions(permissions, new CheckRequestPermissionsListener() { 34 | @Override 35 | public void onAllPermissionOk(Permission[] allPermissions) { 36 | downloadInternal(url, filePathName, downloadListener); 37 | } 38 | 39 | @Override 40 | public void onPermissionDenied(Permission[] refusedPermissions) { 41 | 42 | } 43 | }); 44 | } 45 | 46 | private void downloadInternal(String url, String filePathName, IDownloadListener downloadListener) { 47 | mDownloadProgressObserver = new DownloadProgressObserver(filePathName, downloadListener); 48 | UpdaterApi.getDownloadService(downloadListener).download(url) 49 | .subscribeOn(Schedulers.io()) 50 | .unsubscribeOn(Schedulers.io()) 51 | .doOnError(throwable -> { 52 | if (downloadListener != null) { 53 | AndroidSchedulers.mainThread().createWorker().schedule(() -> { 54 | downloadListener.onError(throwable); 55 | }); 56 | } 57 | }) 58 | .map(ResponseBody::byteStream) 59 | .doOnNext(inputStream -> writeFile(inputStream, filePathName)) 60 | .observeOn(AndroidSchedulers.mainThread()) 61 | .subscribe(mDownloadProgressObserver); 62 | } 63 | 64 | void cancel() { 65 | if (mDownloadProgressObserver != null) { 66 | mDownloadProgressObserver.dispose(); 67 | } 68 | } 69 | 70 | private void writeFile(InputStream inputStream, String filePathName) throws Exception { 71 | File file = new File(filePathName); 72 | if (file.exists()) { 73 | file.delete(); 74 | } 75 | 76 | FileOutputStream fos = new FileOutputStream(file); 77 | byte[] buffer = new byte[2048]; 78 | int len; 79 | while ((len = inputStream.read(buffer)) != -1) { 80 | fos.write(buffer, 0, len); 81 | } 82 | inputStream.close(); 83 | fos.close(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /updater/src/main/java/com/sunfusheng/UpdaterNotification.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng; 2 | 3 | import android.app.NotificationChannel; 4 | import android.app.NotificationManager; 5 | import android.app.PendingIntent; 6 | import android.content.Context; 7 | import android.content.Intent; 8 | import android.graphics.drawable.BitmapDrawable; 9 | import android.graphics.drawable.Drawable; 10 | import android.os.Build; 11 | 12 | import androidx.core.app.NotificationCompat; 13 | 14 | import static android.os.Build.VERSION.SDK_INT; 15 | 16 | /** 17 | * @author sunfusheng 18 | * @since 2019-09-19 19 | */ 20 | class UpdaterNotification { 21 | private static int NOTIFICATION_ID = 8888; 22 | private static String CHANNEL_ID = "UpdaterNotification"; 23 | private static String CHANNEL_NAME = "FirUpdater"; 24 | private static int CHANNEL_SEQUENCE = 0; 25 | private static String LAST_CHANNEL_ID = CHANNEL_ID; 26 | 27 | private NotificationManager mNotificationManager; 28 | private NotificationCompat.Builder mNotificationBuilder; 29 | 30 | UpdaterNotification(Context context, String title) { 31 | mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); 32 | setChannel(); 33 | mNotificationBuilder = new NotificationCompat.Builder(context, CHANNEL_ID); 34 | mNotificationBuilder.setContentTitle(title); 35 | mNotificationBuilder.setSmallIcon(android.R.drawable.stat_sys_download); 36 | Drawable icon = UpdaterUtil.getIcon(context); 37 | if (icon instanceof BitmapDrawable) { 38 | mNotificationBuilder.setLargeIcon(((BitmapDrawable) icon).getBitmap()); 39 | } 40 | mNotificationBuilder.setAutoCancel(false); 41 | mNotificationBuilder.setOngoing(true); 42 | mNotificationBuilder.setProgress(100, 0, false); 43 | mNotificationBuilder.setWhen(System.currentTimeMillis()); 44 | PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, new Intent(), PendingIntent.FLAG_UPDATE_CURRENT); 45 | mNotificationBuilder.setFullScreenIntent(pendingIntent, false); 46 | mNotificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); 47 | mNotificationBuilder.setContentIntent(pendingIntent); 48 | mNotificationBuilder.setVibrate(new long[]{0}); 49 | mNotificationBuilder.setPriority(NotificationCompat.PRIORITY_LOW); 50 | } 51 | 52 | private void setChannel() { 53 | if (SDK_INT >= Build.VERSION_CODES.O) { 54 | mNotificationManager.deleteNotificationChannel(LAST_CHANNEL_ID); 55 | CHANNEL_SEQUENCE++; 56 | CHANNEL_ID = CHANNEL_ID + CHANNEL_SEQUENCE; 57 | CHANNEL_NAME = CHANNEL_NAME + CHANNEL_SEQUENCE; 58 | LAST_CHANNEL_ID = CHANNEL_ID; 59 | NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW); 60 | notificationChannel.enableVibration(false); 61 | notificationChannel.setVibrationPattern(new long[]{0}); 62 | mNotificationManager.createNotificationChannel(notificationChannel); 63 | } 64 | } 65 | 66 | void setProgress(int progress) { 67 | mNotificationBuilder.setContentText("下载更新中..." + progress + "%"); 68 | mNotificationBuilder.setProgress(100, progress, false); 69 | mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); 70 | } 71 | 72 | void setContentIntent(Context context, Intent intent) { 73 | PendingIntent pIntent = PendingIntent.getActivity(context, 1, intent, 0); 74 | mNotificationBuilder.setContentIntent(pIntent); 75 | } 76 | 77 | void cancel() { 78 | mNotificationManager.notify(NOTIFICATION_ID, mNotificationBuilder.build()); 79 | mNotificationManager.cancel(NOTIFICATION_ID); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /updater/src/main/java/com/sunfusheng/UpdaterService.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng; 2 | 3 | import io.reactivex.Observable; 4 | import retrofit2.http.GET; 5 | import retrofit2.http.Path; 6 | import retrofit2.http.Query; 7 | 8 | /** 9 | * @author by sunfusheng on 2019-08-13 10 | */ 11 | interface UpdaterService { 12 | String BASE_URL = "http://api.fir.im/"; 13 | 14 | @GET("apps/latest/{app_id}") 15 | Observable fetchAppInfo( 16 | @Path("app_id") String appId, 17 | @Query("api_token") String apiToken 18 | ); 19 | } -------------------------------------------------------------------------------- /updater/src/main/java/com/sunfusheng/UpdaterUtil.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.ActivityManager; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.SharedPreferences; 8 | import android.content.pm.PackageInfo; 9 | import android.content.pm.PackageManager; 10 | import android.graphics.drawable.Drawable; 11 | import android.net.Uri; 12 | import android.os.Build; 13 | import android.os.Handler; 14 | import android.os.Looper; 15 | import android.text.TextUtils; 16 | 17 | import androidx.core.content.FileProvider; 18 | 19 | import java.io.Closeable; 20 | import java.io.File; 21 | import java.io.FileNotFoundException; 22 | import java.io.IOException; 23 | import java.util.List; 24 | 25 | /** 26 | * @author sunfusheng 27 | * @since 2019-08-14 28 | */ 29 | public class UpdaterUtil { 30 | 31 | public static String getPackageName(Context context) { 32 | return context.getPackageName(); 33 | } 34 | 35 | public static String getName(Context context) { 36 | try { 37 | PackageManager pm = context.getPackageManager(); 38 | PackageInfo pi = pm.getPackageInfo(getPackageName(context), 0); 39 | return pi.applicationInfo.loadLabel(pm).toString(); 40 | } catch (PackageManager.NameNotFoundException e) { 41 | e.printStackTrace(); 42 | return null; 43 | } 44 | } 45 | 46 | public static Drawable getIcon(Context context) { 47 | try { 48 | PackageManager pm = context.getPackageManager(); 49 | PackageInfo pi = pm.getPackageInfo(getPackageName(context), 0); 50 | return pi.applicationInfo.loadIcon(pm); 51 | } catch (PackageManager.NameNotFoundException e) { 52 | e.printStackTrace(); 53 | return null; 54 | } 55 | } 56 | 57 | public static int getVersionCode(Context context) { 58 | return getVersionCode(context, getPackageName(context)); 59 | } 60 | 61 | public static int getVersionCode(Context context, String packageName) { 62 | try { 63 | packageName = TextUtils.isEmpty(packageName) ? getPackageName(context) : packageName; 64 | PackageManager pm = context.getPackageManager(); 65 | PackageInfo pi = pm.getPackageInfo(packageName, 0); 66 | return pi == null ? -1 : pi.versionCode; 67 | } catch (PackageManager.NameNotFoundException e) { 68 | e.printStackTrace(); 69 | return -1; 70 | } 71 | } 72 | 73 | public static String getVersionName(Context context) { 74 | return getVersionName(context, getPackageName(context)); 75 | } 76 | 77 | public static String getVersionName(Context context, String packageName) { 78 | try { 79 | PackageManager pm = context.getPackageManager(); 80 | PackageInfo pi = pm.getPackageInfo(packageName, 0); 81 | return pi == null ? null : pi.versionName; 82 | } catch (PackageManager.NameNotFoundException e) { 83 | e.printStackTrace(); 84 | return null; 85 | } 86 | } 87 | 88 | public static boolean isAppForeground(Context context) { 89 | ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 90 | if (manager == null) { 91 | return false; 92 | } 93 | 94 | List appProcesses = manager.getRunningAppProcesses(); 95 | if (appProcesses == null || appProcesses.size() == 0) { 96 | return false; 97 | } 98 | 99 | for (ActivityManager.RunningAppProcessInfo processInfo : appProcesses) { 100 | if (processInfo == null) continue; 101 | if (processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { 102 | return processInfo.processName.equals(getPackageName(context)); 103 | } 104 | } 105 | return false; 106 | } 107 | 108 | public static Intent getInstallApkIntent(Context context, String apkPath) { 109 | try { 110 | File apkFile = new File(apkPath); 111 | if (!apkFile.exists()) { 112 | throw new FileNotFoundException("Apk file does not exist!"); 113 | } 114 | 115 | Intent intent = new Intent(Intent.ACTION_VIEW); 116 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 117 | Uri apkUri; 118 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 119 | apkUri = FileProvider.getUriForFile(context, getPackageName(context) + ".file_provider", apkFile); 120 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 121 | } else { 122 | apkUri = Uri.fromFile(apkFile); 123 | } 124 | intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); 125 | return intent; 126 | } catch (Exception e) { 127 | e.printStackTrace(); 128 | } 129 | return null; 130 | } 131 | 132 | public static void installApk(Context context, String apkPath) { 133 | context.startActivity(getInstallApkIntent(context, apkPath)); 134 | } 135 | 136 | public static void closeQuietly(final Closeable... closeables) { 137 | if (closeables == null || closeables.length == 0) { 138 | return; 139 | } 140 | 141 | for (Closeable closeable : closeables) { 142 | if (closeable == null) { 143 | continue; 144 | } 145 | 146 | try { 147 | closeable.close(); 148 | } catch (IOException ignored) { 149 | } 150 | } 151 | } 152 | 153 | @SuppressLint("DefaultLocale") 154 | public static String getFileSizeDesc(long byteCount) { 155 | if (byteCount <= 0) { 156 | return "0B"; 157 | } else if (byteCount < 1024) { 158 | return String.format("%.2fB", (double) byteCount); 159 | } else if (byteCount < 1048576) { 160 | return String.format("%.2fKB", (double) byteCount / 1024); 161 | } else if (byteCount < 1073741824) { 162 | return String.format("%.2fMB", (double) byteCount / 1048576); 163 | } else { 164 | return String.format("%.2fGB", (double) byteCount / 1073741824); 165 | } 166 | } 167 | 168 | private static final Handler handler = new Handler(Looper.getMainLooper()); 169 | 170 | public static void runOnMainThread(Runnable runnable) { 171 | if (runnable != null) { 172 | if (Looper.myLooper() != Looper.getMainLooper()) { 173 | handler.post(runnable); 174 | } else { 175 | runnable.run(); 176 | } 177 | } 178 | } 179 | 180 | private static SharedPreferences sharedPreferences; 181 | private static final String SP_NAME = "fir_downloader_progress_file_name"; 182 | 183 | public static SharedPreferences getSharedPreferences(Context context) { 184 | if (sharedPreferences == null) { 185 | sharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE); 186 | } 187 | return sharedPreferences; 188 | } 189 | 190 | public static void putCurrLengthValue(Context context, String key, int value) { 191 | getSharedPreferences(context) 192 | .edit() 193 | .putInt(key, value) 194 | .apply(); 195 | } 196 | 197 | public static int getCurrLengthValue(Context context, String key) { 198 | return getSharedPreferences(context).getInt(key, 0); 199 | } 200 | 201 | public static boolean isAppInstalled(Context context, String appPackageName) { 202 | PackageManager manager = context.getPackageManager(); 203 | if (manager == null) { 204 | return false; 205 | } 206 | 207 | List infos = manager.getInstalledPackages(0); 208 | if (infos != null) { 209 | for (int i = 0; i < infos.size(); i++) { 210 | String packageName = infos.get(i).packageName; 211 | if (packageName.equals(appPackageName)) { 212 | return true; 213 | } 214 | } 215 | } 216 | return false; 217 | } 218 | 219 | public static boolean startApp(Context context, String appPackageName) { 220 | try { 221 | PackageManager manager = context.getPackageManager(); 222 | if (manager == null) { 223 | return false; 224 | } 225 | 226 | Intent intent = manager.getLaunchIntentForPackage(appPackageName); 227 | if (intent != null) { 228 | context.startActivity(intent); 229 | return true; 230 | } 231 | } catch (Exception e) { 232 | e.printStackTrace(); 233 | } 234 | return false; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /updater/src/main/java/com/sunfusheng/download/DownloadInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng.download; 2 | 3 | import java.io.IOException; 4 | 5 | import okhttp3.Interceptor; 6 | import okhttp3.Response; 7 | 8 | /** 9 | * @author sunfusheng 10 | * @since 2019-09-16 11 | */ 12 | public class DownloadInterceptor implements Interceptor { 13 | private IDownloadListener mDownloadListener; 14 | 15 | public DownloadInterceptor(IDownloadListener downloadListener) { 16 | this.mDownloadListener = downloadListener; 17 | } 18 | 19 | @Override 20 | public Response intercept(Chain chain) throws IOException { 21 | Response response = chain.proceed(chain.request()); 22 | return response.newBuilder() 23 | .body(new DownloadProgressResponseBody(response.body(), mDownloadListener)) 24 | .build(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /updater/src/main/java/com/sunfusheng/download/DownloadProgressObserver.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng.download; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | 6 | import java.io.File; 7 | import java.io.InputStream; 8 | import java.lang.ref.WeakReference; 9 | 10 | import io.reactivex.Observer; 11 | import io.reactivex.disposables.Disposable; 12 | 13 | /** 14 | * @author sunfusheng 15 | * @since 2019-09-16 16 | */ 17 | public class DownloadProgressObserver implements Observer { 18 | private static final Handler mMainThreadHandler = new Handler(Looper.getMainLooper()); 19 | private WeakReference mDisposableWeakReference; 20 | private String mFilePath; 21 | private IDownloadListener mDownloadListener; 22 | 23 | public DownloadProgressObserver(String filePath, IDownloadListener downloadListener) { 24 | this.mFilePath = filePath; 25 | this.mDownloadListener = downloadListener; 26 | } 27 | 28 | @Override 29 | public void onSubscribe(Disposable disposable) { 30 | mDisposableWeakReference = new WeakReference<>(disposable); 31 | if (mDownloadListener != null) { 32 | mMainThreadHandler.post(() -> mDownloadListener.onStart()); 33 | } 34 | } 35 | 36 | @Override 37 | public void onNext(InputStream inputStream) { 38 | } 39 | 40 | @Override 41 | public void onError(Throwable e) { 42 | dispose(); 43 | if (mDownloadListener != null) { 44 | mMainThreadHandler.post(() -> mDownloadListener.onError(e)); 45 | } 46 | } 47 | 48 | @Override 49 | public void onComplete() { 50 | dispose(); 51 | if (mDownloadListener != null) { 52 | mMainThreadHandler.post(() -> mDownloadListener.onSuccess(new File(mFilePath))); 53 | } 54 | } 55 | 56 | public void dispose() { 57 | if (mDisposableWeakReference != null) { 58 | Disposable disposable = mDisposableWeakReference.get(); 59 | if (disposable != null && !disposable.isDisposed()) { 60 | disposable.dispose(); 61 | } 62 | mDisposableWeakReference = null; 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /updater/src/main/java/com/sunfusheng/download/DownloadProgressResponseBody.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng.download; 2 | 3 | import java.io.IOException; 4 | 5 | import io.reactivex.android.schedulers.AndroidSchedulers; 6 | import okhttp3.MediaType; 7 | import okhttp3.ResponseBody; 8 | import okio.Buffer; 9 | import okio.BufferedSource; 10 | import okio.ForwardingSource; 11 | import okio.Okio; 12 | import okio.Source; 13 | 14 | /** 15 | * @author sunfusheng 16 | * @since 2019-09-16 17 | */ 18 | public class DownloadProgressResponseBody extends ResponseBody { 19 | private ResponseBody mResponseBody; 20 | private IDownloadListener mDownloadListener; 21 | private BufferedSource mBufferedSource; 22 | 23 | public DownloadProgressResponseBody(ResponseBody responseBody, IDownloadListener downloadListener) { 24 | this.mResponseBody = responseBody; 25 | this.mDownloadListener = downloadListener; 26 | } 27 | 28 | @Override 29 | public MediaType contentType() { 30 | return mResponseBody.contentType(); 31 | } 32 | 33 | @Override 34 | public long contentLength() { 35 | return mResponseBody.contentLength(); 36 | } 37 | 38 | @Override 39 | public BufferedSource source() { 40 | if (mBufferedSource == null) { 41 | mBufferedSource = Okio.buffer(source(mResponseBody.source())); 42 | } 43 | return mBufferedSource; 44 | } 45 | 46 | private Source source(Source source) { 47 | return new ForwardingSource(source) { 48 | long totalBytesRead = 0L; 49 | 50 | @Override 51 | public long read(Buffer sink, long byteCount) throws IOException { 52 | long bytesRead = super.read(sink, byteCount); 53 | totalBytesRead += bytesRead != -1 ? bytesRead : 0; 54 | if (mDownloadListener != null && contentLength() > 0) { 55 | int percentage = (int) (totalBytesRead * 100 / contentLength()); 56 | if (bytesRead != -1) { 57 | AndroidSchedulers.mainThread().createWorker().schedule(() -> { 58 | mDownloadListener.onProgress(totalBytesRead, contentLength(), percentage); 59 | }); 60 | } 61 | 62 | } 63 | return bytesRead; 64 | } 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /updater/src/main/java/com/sunfusheng/download/DownloadService.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng.download; 2 | 3 | import io.reactivex.Observable; 4 | import okhttp3.ResponseBody; 5 | import retrofit2.http.GET; 6 | import retrofit2.http.Streaming; 7 | import retrofit2.http.Url; 8 | 9 | /** 10 | * @author sunfusheng 11 | * @since 2019-09-16 12 | */ 13 | public interface DownloadService { 14 | @Streaming 15 | @GET 16 | Observable download(@Url String url); 17 | } 18 | -------------------------------------------------------------------------------- /updater/src/main/java/com/sunfusheng/download/IDownloadListener.java: -------------------------------------------------------------------------------- 1 | package com.sunfusheng.download; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * @author sunfusheng 7 | * @since 2019-09-16 8 | */ 9 | public interface IDownloadListener { 10 | void onStart(); 11 | 12 | void onProgress(long bytesTransferred, long totalBytes, int percentage); 13 | 14 | void onSuccess(File file); 15 | 16 | void onError(Throwable e); 17 | } 18 | -------------------------------------------------------------------------------- /updater/src/main/res/xml/file_provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | --------------------------------------------------------------------------------