Compare commits

...

24 Commits

Author SHA1 Message Date
Jenly ad203b292d Update README 3 years ago
Jenly 00fd88c259 maven publish 3 years ago
Jenly ed24593d5c 优化细节 4 years ago
Jenly b77f795b3d 优化细节 4 years ago
Jenly 2263780073 优化细节 4 years ago
Jenly cbb98f0ff5 优化细节 4 years ago
Jenly 165709991f * AppDialogConfig添加构造参数,简化自定义扩展用法 4 years ago
Jenly 6d6848ed94 优化细节 4 years ago
Jenly 0f335e154b 优化细节 4 years ago
Jenly 23a99f6cd9 * 优化默认Dialog样式的显示细节 4 years ago
Jenly dc5ff74a9e 优化细节 4 years ago
jenly1314 21d9d4215e 支持MD5校验 5 years ago
jenly1314 46c743cb26 支持MD5校验 5 years ago
jenly1314 888ce12ddc 支持重定向 (#7) 5 years ago
jenly1314 32e71c030e 优化细节 5 years ago
jenly1314 946b6e8550 增加混淆 5 years ago
jenly1314 d7a9bd8dd0 优化细节 5 years ago
jenly1314 6dee0be157 优化细节 5 years ago
jenly1314 b68626587a CI 5 years ago
jenly1314 bd74ae12ce 支持取消下载 5 years ago
jenly1314 62210f4b88 支持取消下载 5 years ago
jenly1314 76f489f8d2 CI 5 years ago
jenly1314 4f4e977604 CI 5 years ago
jenly1314 38d7641303 支持AndroidX版本 5 years ago
  1. 27
      .circleci/config.yml
  2. 4
      .gitignore
  3. BIN
      .idea/caches/build_file_checksums.ser
  4. 52
      .idea/codeStyles/Project.xml
  5. 8
      .idea/compiler.xml
  6. 456
      .idea/dbnavigator.xml
  7. 6
      .idea/encodings.xml
  8. 20
      .idea/gradle.xml
  9. 83
      .idea/markdown-navigator.xml
  10. 3
      .idea/markdown-navigator/profiles_settings.xml
  11. 38
      .idea/misc.xml
  12. 12
      .idea/runConfigurations.xml
  13. 6
      .idea/vcs.xml
  14. 87
      README.md
  15. 4
      app-dialog/build.gradle
  16. 3
      app-dialog/gradle.properties
  17. 8
      app-dialog/proguard-rules.pro
  18. 233
      app-dialog/src/main/java/com/king/app/dialog/AppDialog.java
  19. 227
      app-dialog/src/main/java/com/king/app/dialog/AppDialogConfig.java
  20. 251
      app-dialog/src/main/java/com/king/app/dialog/BaseDialogConfig.java
  21. 45
      app-dialog/src/main/java/com/king/app/dialog/fragment/AppDialogFragment.java
  22. 3
      app-dialog/src/main/java/com/king/app/dialog/fragment/BaseDialogFragment.java
  23. 6
      app-dialog/src/main/res/layout/app_dialog.xml
  24. 2
      app-dialog/src/main/res/values/colors.xml
  25. 2
      app-dialog/src/main/res/values/dimens.xml
  26. 2
      app-dialog/src/main/res/values/strings.xml
  27. 4
      app-updater/build.gradle
  28. 3
      app-updater/gradle.properties
  29. 10
      app-updater/proguard-rules.pro
  30. 4
      app-updater/src/main/AndroidManifest.xml
  31. 88
      app-updater/src/main/java/com/king/app/updater/AppUpdater.java
  32. 51
      app-updater/src/main/java/com/king/app/updater/UpdateConfig.java
  33. 2
      app-updater/src/main/java/com/king/app/updater/callback/UpdateCallback.java
  34. 11
      app-updater/src/main/java/com/king/app/updater/constant/Constants.java
  35. 134
      app-updater/src/main/java/com/king/app/updater/http/HttpManager.java
  36. 7
      app-updater/src/main/java/com/king/app/updater/http/IHttpManager.java
  37. 231
      app-updater/src/main/java/com/king/app/updater/http/OkHttpManager.java
  38. 356
      app-updater/src/main/java/com/king/app/updater/service/DownloadService.java
  39. 134
      app-updater/src/main/java/com/king/app/updater/util/AppUtils.java
  40. 222
      app-updater/src/main/java/com/king/app/updater/util/NotificationUtils.java
  41. 17
      app-updater/src/main/java/com/king/app/updater/util/PermissionUtils.java
  42. 12
      app-updater/src/main/java/com/king/app/updater/util/SSLSocketFactoryUtils.java
  43. 2
      app-updater/src/main/res/values/strings.xml
  44. 2
      app-updater/src/main/res/xml/app_updater_paths.xml
  45. 1
      app/build.gradle
  46. BIN
      app/release/app-release.apk
  47. 2
      app/release/output.json
  48. 101
      app/src/main/java/com/king/appupdater/MainActivity.java
  49. 13
      app/src/main/res/layout/activity_main.xml
  50. 9
      app/src/main/res/layout/dialog.xml
  51. 1
      app/src/main/res/layout/dialog_custom.xml
  52. 2
      app/src/main/res/values/styles.xml
  53. 6
      build.gradle
  54. 20
      gradle.properties
  55. 2
      gradle/wrapper/gradle-wrapper.properties
  56. 11
      versions.gradle

@ -0,0 +1,27 @@
version: 2
jobs:
build:
working_directory: ~/code
docker:
- image: circleci/android:api-28
environment:
JVM_OPTS: -Xmx3200m
steps:
- checkout
- restore_cache:
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:
name: Download Dependencies
command: ./gradlew androidDependencies
- save_cache:
paths:
- ~/.gradle
key: jars-{{ checksum "build.gradle" }}-{{ checksum "app/build.gradle" }}
- run:
name: Run Tests
command: ./gradlew lint test
- store_artifacts:
path: app/build/reports
destination: reports
- store_test_results:
path: app/build/test-results

4
.gitignore vendored

@ -1,9 +1,7 @@
*.iml
.gradle
/local.properties
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea
.DS_Store
/build
/captures

@ -1,52 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<DBN-PSQL>
<case-options enabled="true">
<option name="KEYWORD_CASE" value="lower" />
<option name="FUNCTION_CASE" value="lower" />
<option name="PARAMETER_CASE" value="lower" />
<option name="DATATYPE_CASE" value="lower" />
<option name="OBJECT_CASE" value="preserve" />
</case-options>
<formatting-settings enabled="false" />
</DBN-PSQL>
<DBN-SQL>
<case-options enabled="true">
<option name="KEYWORD_CASE" value="lower" />
<option name="FUNCTION_CASE" value="lower" />
<option name="PARAMETER_CASE" value="lower" />
<option name="DATATYPE_CASE" value="lower" />
<option name="OBJECT_CASE" value="preserve" />
</case-options>
<formatting-settings enabled="false">
<option name="STATEMENT_SPACING" value="one_line" />
<option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
<option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
</formatting-settings>
</DBN-SQL>
<DBN-PSQL>
<case-options enabled="true">
<option name="KEYWORD_CASE" value="lower" />
<option name="FUNCTION_CASE" value="lower" />
<option name="PARAMETER_CASE" value="lower" />
<option name="DATATYPE_CASE" value="lower" />
<option name="OBJECT_CASE" value="preserve" />
</case-options>
<formatting-settings enabled="false" />
</DBN-PSQL>
<DBN-SQL>
<case-options enabled="true">
<option name="KEYWORD_CASE" value="lower" />
<option name="FUNCTION_CASE" value="lower" />
<option name="PARAMETER_CASE" value="lower" />
<option name="DATATYPE_CASE" value="lower" />
<option name="OBJECT_CASE" value="preserve" />
</case-options>
<formatting-settings enabled="false">
<option name="STATEMENT_SPACING" value="one_line" />
<option name="CLAUSE_CHOP_DOWN" value="chop_down_if_statement_long" />
<option name="ITERATION_ELEMENTS_WRAPPING" value="chop_down_if_not_single" />
</formatting-settings>
</DBN-SQL>
</code_scheme>
</component>

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
</annotationProcessing>
</component>
</project>

@ -1,456 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DBNavigator.Project.DataEditorManager">
<record-view-column-sorting-type value="BY_INDEX" />
<value-preview-text-wrapping value="true" />
<value-preview-pinned value="false" />
</component>
<component name="DBNavigator.Project.DataExportManager">
<export-instructions>
<create-header value="true" />
<quote-values-containing-separator value="true" />
<quote-all-values value="false" />
<value-separator value="" />
<file-name value="" />
<file-location value="" />
<scope value="GLOBAL" />
<destination value="FILE" />
<format value="EXCEL" />
<charset value="GBK" />
</export-instructions>
</component>
<component name="DBNavigator.Project.DatabaseBrowserManager">
<autoscroll-to-editor value="false" />
<autoscroll-from-editor value="true" />
<show-object-properties value="true" />
<loaded-nodes />
</component>
<component name="DBNavigator.Project.DatabaseFileManager">
<open-files />
</component>
<component name="DBNavigator.Project.EditorStateManager">
<last-used-providers />
</component>
<component name="DBNavigator.Project.MethodExecutionManager">
<method-browser />
<execution-history>
<group-entries value="true" />
<execution-inputs />
</execution-history>
<argument-values-cache />
</component>
<component name="DBNavigator.Project.ObjectDependencyManager">
<last-used-dependency-type value="INCOMING" />
</component>
<component name="DBNavigator.Project.ObjectQuickFilterManager">
<last-used-operator value="EQUAL" />
<filters />
</component>
<component name="DBNavigator.Project.ScriptExecutionManager" clear-outputs="true">
<recently-used-interfaces />
</component>
<component name="DBNavigator.Project.Settings">
<connections />
<browser-settings>
<general>
<display-mode value="TABBED" />
<navigation-history-size value="100" />
<show-object-details value="false" />
</general>
<filters>
<object-type-filter>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="true" />
<object-type name="ROLE" enabled="true" />
<object-type name="PRIVILEGE" enabled="true" />
<object-type name="CHARSET" enabled="true" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED_VIEW" enabled="true" />
<object-type name="NESTED_TABLE" enabled="true" />
<object-type name="COLUMN" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET_TRIGGER" enabled="true" />
<object-type name="DATABASE_TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="true" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
<object-type name="ARGUMENT" enabled="true" />
<object-type name="DIMENSION" enabled="true" />
<object-type name="CLUSTER" enabled="true" />
<object-type name="DBLINK" enabled="true" />
</object-type-filter>
</filters>
<sorting>
<object-type name="COLUMN" sorting-type="NAME" />
<object-type name="FUNCTION" sorting-type="NAME" />
<object-type name="PROCEDURE" sorting-type="NAME" />
<object-type name="ARGUMENT" sorting-type="POSITION" />
</sorting>
<default-editors>
<object-type name="VIEW" editor-type="SELECTION" />
<object-type name="PACKAGE" editor-type="SELECTION" />
<object-type name="TYPE" editor-type="SELECTION" />
</default-editors>
</browser-settings>
<navigation-settings>
<lookup-filters>
<lookup-objects>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="false" />
<object-type name="ROLE" enabled="false" />
<object-type name="PRIVILEGE" enabled="false" />
<object-type name="CHARSET" enabled="false" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED VIEW" enabled="true" />
<object-type name="NESTED TABLE" enabled="false" />
<object-type name="COLUMN" enabled="false" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET TRIGGER" enabled="true" />
<object-type name="DATABASE TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="false" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE ATTRIBUTE" enabled="false" />
<object-type name="ARGUMENT" enabled="false" />
<object-type name="DIMENSION" enabled="false" />
<object-type name="CLUSTER" enabled="false" />
<object-type name="DBLINK" enabled="true" />
</lookup-objects>
<force-database-load value="false" />
<prompt-connection-selection value="true" />
<prompt-schema-selection value="true" />
</lookup-filters>
</navigation-settings>
<dataset-grid-settings>
<general>
<enable-zooming value="true" />
<enable-column-tooltip value="true" />
</general>
<sorting>
<nulls-first value="true" />
<max-sorting-columns value="4" />
</sorting>
<tracking-columns>
<columnNames value="" />
<visible value="true" />
<editable value="false" />
</tracking-columns>
</dataset-grid-settings>
<dataset-editor-settings>
<text-editor-popup>
<active value="false" />
<active-if-empty value="false" />
<data-length-threshold value="100" />
<popup-delay value="1000" />
</text-editor-popup>
<values-actions-popup>
<show-popup-button value="true" />
<element-count-threshold value="1000" />
<data-length-threshold value="250" />
</values-actions-popup>
<general>
<fetch-block-size value="100" />
<fetch-timeout value="30" />
<trim-whitespaces value="true" />
<convert-empty-strings-to-null value="true" />
<select-content-on-cell-edit value="true" />
<large-value-preview-active value="true" />
</general>
<filters>
<prompt-filter-dialog value="true" />
<default-filter-type value="BASIC" />
</filters>
<qualified-text-editor text-length-threshold="300">
<content-types>
<content-type name="Text" enabled="true" />
<content-type name="Properties" enabled="true" />
<content-type name="XML" enabled="true" />
<content-type name="DTD" enabled="true" />
<content-type name="HTML" enabled="true" />
<content-type name="XHTML" enabled="true" />
<content-type name="Java" enabled="true" />
<content-type name="SQL" enabled="true" />
<content-type name="PL/SQL" enabled="true" />
<content-type name="Groovy" enabled="true" />
<content-type name="AIDL" enabled="true" />
<content-type name="YAML" enabled="true" />
<content-type name="Manifest" enabled="true" />
</content-types>
</qualified-text-editor>
<record-navigation>
<navigation-target value="VIEWER" />
</record-navigation>
</dataset-editor-settings>
<code-editor-settings>
<general>
<show-object-navigation-gutter value="false" />
<show-spec-declaration-navigation-gutter value="true" />
<enable-spellchecking value="true" />
<enable-reference-spellchecking value="false" />
</general>
<confirmations>
<save-changes value="false" />
<revert-changes value="true" />
</confirmations>
</code-editor-settings>
<code-completion-settings>
<filters>
<basic-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="false" />
<filter-element type="OBJECT" id="view" selected="false" />
<filter-element type="OBJECT" id="materialized view" selected="false" />
<filter-element type="OBJECT" id="index" selected="false" />
<filter-element type="OBJECT" id="constraint" selected="false" />
<filter-element type="OBJECT" id="trigger" selected="false" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="false" />
<filter-element type="OBJECT" id="procedure" selected="false" />
<filter-element type="OBJECT" id="function" selected="false" />
<filter-element type="OBJECT" id="package" selected="false" />
<filter-element type="OBJECT" id="type" selected="false" />
<filter-element type="OBJECT" id="dimension" selected="false" />
<filter-element type="OBJECT" id="cluster" selected="false" />
<filter-element type="OBJECT" id="dblink" selected="false" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</basic-filter>
<extended-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</extended-filter>
</filters>
<sorting enabled="true">
<sorting-element type="RESERVED_WORD" id="keyword" />
<sorting-element type="RESERVED_WORD" id="datatype" />
<sorting-element type="OBJECT" id="column" />
<sorting-element type="OBJECT" id="table" />
<sorting-element type="OBJECT" id="view" />
<sorting-element type="OBJECT" id="materialized view" />
<sorting-element type="OBJECT" id="index" />
<sorting-element type="OBJECT" id="constraint" />
<sorting-element type="OBJECT" id="trigger" />
<sorting-element type="OBJECT" id="synonym" />
<sorting-element type="OBJECT" id="sequence" />
<sorting-element type="OBJECT" id="procedure" />
<sorting-element type="OBJECT" id="function" />
<sorting-element type="OBJECT" id="package" />
<sorting-element type="OBJECT" id="type" />
<sorting-element type="OBJECT" id="dimension" />
<sorting-element type="OBJECT" id="cluster" />
<sorting-element type="OBJECT" id="dblink" />
<sorting-element type="OBJECT" id="schema" />
<sorting-element type="OBJECT" id="role" />
<sorting-element type="OBJECT" id="user" />
<sorting-element type="RESERVED_WORD" id="function" />
<sorting-element type="RESERVED_WORD" id="parameter" />
</sorting>
<format>
<enforce-code-style-case value="true" />
</format>
</code-completion-settings>
<execution-engine-settings>
<statement-execution>
<fetch-block-size value="100" />
<execution-timeout value="20" />
<debug-execution-timeout value="600" />
<focus-result value="false" />
<prompt-execution value="false" />
</statement-execution>
<script-execution>
<command-line-interfaces />
<execution-timeout value="300" />
</script-execution>
<method-execution>
<execution-timeout value="30" />
<debug-execution-timeout value="600" />
<parameter-history-size value="10" />
</method-execution>
</execution-engine-settings>
<operation-settings>
<transactions>
<uncommitted-changes>
<on-project-close value="ASK" />
<on-disconnect value="ASK" />
<on-autocommit-toggle value="ASK" />
</uncommitted-changes>
<multiple-uncommitted-changes>
<on-commit value="ASK" />
<on-rollback value="ASK" />
</multiple-uncommitted-changes>
</transactions>
<session-browser>
<disconnect-session value="ASK" />
<kill-session value="ASK" />
<reload-on-filter-change value="false" />
</session-browser>
<compiler>
<compile-type value="KEEP" />
<compile-dependencies value="ASK" />
<always-show-controls value="false" />
</compiler>
<debugger>
<debugger-type value="ASK" />
<use-generic-runners value="true" />
</debugger>
</operation-settings>
<ddl-file-settings>
<extensions>
<mapping file-type-id="VIEW" extensions="vw" />
<mapping file-type-id="TRIGGER" extensions="trg" />
<mapping file-type-id="PROCEDURE" extensions="prc" />
<mapping file-type-id="FUNCTION" extensions="fnc" />
<mapping file-type-id="PACKAGE" extensions="pkg" />
<mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
<mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
<mapping file-type-id="TYPE" extensions="tpe" />
<mapping file-type-id="TYPE_SPEC" extensions="tps" />
<mapping file-type-id="TYPE_BODY" extensions="tpb" />
</extensions>
<general>
<lookup-ddl-files value="true" />
<create-ddl-files value="false" />
<synchronize-ddl-files value="true" />
<use-qualified-names value="false" />
<make-scripts-rerunnable value="true" />
</general>
</ddl-file-settings>
<general-settings>
<regional-settings>
<date-format value="MEDIUM" />
<number-format value="UNGROUPED" />
<locale value="SYSTEM_DEFAULT" />
<use-custom-formats value="false" />
</regional-settings>
<environment>
<environment-types>
<environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
<environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
<environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
<environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
</environment-types>
<visibility-settings>
<connection-tabs value="true" />
<dialog-headers value="true" />
<object-editor-tabs value="true" />
<script-editor-tabs value="false" />
<execution-result-tabs value="true" />
</visibility-settings>
</environment>
</general-settings>
</component>
<component name="DBNavigator.Project.StatementExecutionManager">
<execution-variables />
</component>
</project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="PROJECT" charset="UTF-8" />
</component>
</project>

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/app-dialog" />
<option value="$PROJECT_DIR$/app-updater" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>

@ -1,83 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownProjectSettings" wasCopied="true">
<PreviewSettings splitEditorLayout="SPLIT" splitEditorPreview="PREVIEW" useGrayscaleRendering="false" zoomFactor="1.0" maxImageWidth="0" showGitHubPageIfSynced="false" allowBrowsingInPreview="false" synchronizePreviewPosition="true" highlightPreviewType="NONE" highlightFadeOut="5" highlightOnTyping="true" synchronizeSourcePosition="true" verticallyAlignSourceAndPreviewSyncPosition="true" showSearchHighlightsInPreview="true" showSelectionInPreview="true" openRemoteLinks="true" replaceUnicodeEmoji="false" lastLayoutSetsDefault="false">
<PanelProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.panel" providerName="Default - Swing" />
</PanelProvider>
</PreviewSettings>
<ParserSettings gitHubSyntaxChange="false" emojiShortcuts="0" emojiImages="0">
<PegdownExtensions>
<option name="ABBREVIATIONS" value="false" />
<option name="ANCHORLINKS" value="true" />
<option name="ASIDE" value="false" />
<option name="ATXHEADERSPACE" value="true" />
<option name="AUTOLINKS" value="true" />
<option name="DEFINITIONS" value="false" />
<option name="DEFINITION_BREAK_DOUBLE_BLANK_LINE" value="false" />
<option name="FENCED_CODE_BLOCKS" value="true" />
<option name="FOOTNOTES" value="false" />
<option name="HARDWRAPS" value="false" />
<option name="HTML_DEEP_PARSER" value="false" />
<option name="INSERTED" value="false" />
<option name="QUOTES" value="false" />
<option name="RELAXEDHRULES" value="true" />
<option name="SMARTS" value="false" />
<option name="STRIKETHROUGH" value="true" />
<option name="SUBSCRIPT" value="false" />
<option name="SUPERSCRIPT" value="false" />
<option name="SUPPRESS_HTML_BLOCKS" value="false" />
<option name="SUPPRESS_INLINE_HTML" value="false" />
<option name="TABLES" value="true" />
<option name="TASKLISTITEMS" value="true" />
<option name="TOC" value="false" />
<option name="WIKILINKS" value="true" />
</PegdownExtensions>
<ParserOptions>
<option name="ADMONITION_EXT" value="false" />
<option name="ATTRIBUTES_EXT" value="false" />
<option name="COMMONMARK_LISTS" value="false" />
<option name="DUMMY" value="false" />
<option name="EMOJI_SHORTCUTS" value="true" />
<option name="ENUMERATED_REFERENCES_EXT" value="false" />
<option name="FLEXMARK_FRONT_MATTER" value="false" />
<option name="GFM_LOOSE_BLANK_LINE_AFTER_ITEM_PARA" value="true" />
<option name="GFM_TABLE_RENDERING" value="true" />
<option name="GITBOOK_URL_ENCODING" value="false" />
<option name="GITHUB_LISTS" value="true" />
<option name="GITHUB_WIKI_LINKS" value="true" />
<option name="GITLAB_EXT" value="false" />
<option name="GITLAB_MATH_EXT" value="false" />
<option name="GITLAB_MERMAID_EXT" value="false" />
<option name="HEADER_ID_NON_ASCII_TO_LOWERCASE" value="false" />
<option name="HEADER_ID_NO_DUPED_DASHES" value="false" />
<option name="JEKYLL_FRONT_MATTER" value="false" />
<option name="MACROS_EXT" value="false" />
<option name="NO_TEXT_ATTRIBUTES" value="false" />
<option name="PARSE_HTML_ANCHOR_ID" value="false" />
<option name="SIM_TOC_BLANK_LINE_SPACER" value="true" />
</ParserOptions>
</ParserSettings>
<HtmlSettings headerTopEnabled="false" headerBottomEnabled="false" bodyTopEnabled="false" bodyBottomEnabled="false" embedUrlContent="false" addPageHeader="true" embedImages="false" embedHttpImages="false" imageUriSerials="false" addDocTypeHtml="true" noParaTags="false">
<GeneratorProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.generator" providerName="Default Swing HTML Generator" />
</GeneratorProvider>
<headerTop />
<headerBottom />
<bodyTop />
<bodyBottom />
</HtmlSettings>
<CssSettings previewScheme="UI_SCHEME" cssUri="" isCssUriEnabled="false" isCssUriSerial="false" isCssTextEnabled="false" isDynamicPageWidth="true">
<StylesheetProvider>
<provider providerId="com.vladsch.idea.multimarkdown.editor.swing.html.css" providerName="Default Swing Stylesheet" />
</StylesheetProvider>
<ScriptProviders />
<cssText />
<cssUriHistory />
</CssSettings>
<HtmlExportSettings updateOnSave="false" parentDir="$ProjectFileDir$" targetDir="$ProjectFileDir$" cssDir="" scriptDir="" plainHtml="false" imageDir="" copyLinkedImages="false" imageUniquifyType="0" targetPathType="2" targetExt="" useTargetExt="false" noCssNoScripts="false" useElementStyleAttribute="false" linkToExportedHtml="true" exportOnSettingsChange="true" regenerateOnProjectOpen="false" linkFormatType="HTTP_ABSOLUTE" />
<LinkMapSettings>
<textMaps />
</LinkMapSettings>
</component>
</project>

@ -1,3 +0,0 @@
<component name="MarkdownNavigator.ProfileManager">
<settings default="" pdf-export="" plain-text-search-scope="Project Files" />
</component>

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="7">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="6">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

@ -3,30 +3,33 @@
![Image](app/src/main/ic_launcher-web.png)
[![Download](https://img.shields.io/badge/download-App-blue.svg)](https://raw.githubusercontent.com/jenly1314/AppUpdater/master/app/release/app-release.apk)
[![Jitpack](https://jitpack.io/v/jenly1314/AppUpdater.svg)](https://jitpack.io/#jenly1314/AppUpdater)
[![JCenter](https://img.shields.io/badge/JCenter-1.0.10-46C018.svg)](https://bintray.com/beta/#/jenly/maven/app-updater)
[![JitPack](https://jitpack.io/v/jenly1314/AppUpdater.svg)](https://jitpack.io/#jenly1314/AppUpdater)
[![CI](https://travis-ci.org/jenly1314/AppUpdater.svg?branch=master)](https://travis-ci.org/jenly1314/AppUpdater)
[![CircleCI](https://circleci.com/gh/jenly1314/AppUpdater.svg?style=svg)](https://circleci.com/gh/jenly1314/AppUpdater)
[![API](https://img.shields.io/badge/API-15%2B-blue.svg?style=flat)](https://android-arsenal.com/api?level=15)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/mit-license.php)
[![Blog](https://img.shields.io/badge/blog-Jenly-9933CC.svg)](http://blog.csdn.net/jenly121)
[![Blog](https://img.shields.io/badge/blog-Jenly-9933CC.svg)](https://jenly1314.github.io/)
[![QQGroup](https://img.shields.io/badge/QQGroup-20867961-blue.svg)](http://shang.qq.com/wpa/qunwpa?idkey=8fcc6a2f88552ea44b1411582c94fd124f7bb3ec227e2a400dbbfaad3dc2f5ad)
AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版本升级的开源库。(无需担心通知栏适配;无需担心重复点击下载;无需担心App安装等问题;这些AppUpdater都已帮您处理好。)
AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版本升级的轻量开源库。(无需担心通知栏适配;无需担心重复点击下载;无需担心App安装等问题;这些AppUpdater都已帮您处理好。)
核心库主要包括app-updater和app-dialog。
> 下载更新和弹框提示分开,是因为这本来就是两个逻辑。完全独立开来能有效的解耦。
* app-updater 主要负责后台下载更新App,无需担心下载时各种配置相关的细节,一键傻瓜式升级。
* app-dialog 主要是提供常用的Dialog和DialogFragment,简化弹框提示,样式支持高度自定义。
* app-dialog 主要是提供常用的Dialog和DialogFragment,简化弹框提示,布局样式支持自定义。
> app-updater + app-dialog 配合使用,谁用谁知道。
## 功能介绍
- [x] 专注于App更新一键傻瓜式升级
- [x] 支持下载监听
- [x] 够轻量,体积小
- [x] 支持监听下载过程
- [x] 支持下载失败,重新下载
- [x] 支持下载优先取本地缓存
- [x] 支持下载优先取本地缓存
- [x] 支持通知栏提示内容和过程全部可配置
- [x] 支持Android O
- [x] 支持Android Q(10)
- [x] 支持取消下载
- [x] 支持使用OkHttpClient下载
## Gif 展示
![Image](GIF.gif)
@ -39,7 +42,7 @@ AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版
<dependency>
<groupId>com.king.app</groupId>
<artifactId>app-updater</artifactId>
<version>1.0.4</version>
<version>1.0.10</version>
<type>pom</type>
</dependency>
@ -47,7 +50,7 @@ AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版
<dependency>
<groupId>com.king.app</groupId>
<artifactId>app-dialog</artifactId>
<version>1.0.4</version>
<version>1.0.10</version>
<type>pom</type>
</dependency>
```
@ -56,25 +59,25 @@ AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版
//----------AndroidX 版本
//app-updater
implementation 'com.king.app:app-updater:1.0.4-androidx'
implementation 'com.king.app:app-updater:1.0.10-androidx'
//app-dialog
implementation 'com.king.app:app-dialog:1.0.4-androidx'
implementation 'com.king.app:app-dialog:1.0.10-androidx'
//----------Android 版本
//----------Android Support 版本
//app-updater
implementation 'com.king.app:app-updater:1.0.4'
implementation 'com.king.app:app-updater:1.0.10'
//app-dialog
implementation 'com.king.app:app-dialog:1.0.4'
implementation 'com.king.app:app-dialog:1.0.10'
```
### Lvy:
```lvy
//app-updater
<dependency org='com.king.app' name='app-dialog' rev='1.0.4'>
<dependency org='com.king.app' name='app-dialog' rev='1.0.10'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
//app-dialog
<dependency org='com.king.app' name='app-dialog' rev='1.0.4'>
<dependency org='com.king.app' name='app-dialog' rev='1.0.10'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
```
@ -97,7 +100,7 @@ AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版
```
```Java
//简单弹框升级
AppDialogConfig config = new AppDialogConfig();
AppDialogConfig config = new AppDialogConfig(context);
config.setTitle("简单弹框升级")
.setOk("升级")
.setContent("1、新增某某功能、\n2、修改某某问题、\n3、优化某某BUG、")
@ -105,8 +108,7 @@ AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版
@Override
public void onClick(View v) {
new AppUpdater.Builder()
.serUrl(mUrl)
.setFilename("AppUpdater.apk")
.setUrl(mUrl)
.build(getContext())
.start();
AppDialog.INSTANCE.dismissDialog();
@ -116,7 +118,7 @@ AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版
```
```Java
//简单DialogFragment升级
AppDialogConfig config = new AppDialogConfig();
AppDialogConfig config = new AppDialogConfig(context);
config.setTitle("简单DialogFragment升级")
.setOk("升级")
.setContent("1、新增某某功能、\n2、修改某某问题、\n3、优化某某BUG、")
@ -124,9 +126,10 @@ AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版
@Override
public void onClick(View v) {
new AppUpdater.Builder()
.serUrl(mUrl)
.setUrl(mUrl)
.setFilename("AppUpdater.apk")
.build(getContext())
.setHttpManager(OkHttpManager.getInstance())//不设置HttpManager时,默认使用HttpsURLConnection下载,如果使用OkHttpClient实现下载,需依赖okhttp库
.start();
AppDialog.INSTANCE.dismissDialogFragment(getSupportFragmentManager());
}
@ -135,11 +138,38 @@ AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版
```
更多使用示例请查看[App](app)。
更多使用详情,请查看[app](app)中的源码使用示例或直接查看[API帮助文档](https://jenly1314.github.io/projects/AppUpdater/doc/)
## 混淆
**app-updater** [Proguard rules](app-updater/proguard-rules.pro)
**app-dialog** [Proguard rules](app-dialog/proguard-rules.pro)
## 版本记录
#### v1.0.4:2019-6-4 //支持AndroidX版本
#### v1.0.10:2021-3-4
* AppDialogConfig添加构造参数,简化自定义扩展用法
* 优化细节
#### v1.0.9:2020-12-11
* 优化默认Dialog样式的显示细节
#### v1.0.8:2020-1-2
* 支持MD5校验
* 对外提供获取Dialog方法
#### v1.0.7:2019-12-18
* 优化细节
#### v1.0.6:2019-11-27
* 新增OkHttpManager 如果使用了OkHttpManager则必须依赖[okhttp](https://github.com/square/okhttp)
* 优化细节 (progress,total 变更 int -> long)
#### v1.0.5:2019-9-4
* 支持取消下载
#### v1.0.4:2019-6-4 [开始支持AndroidX版本](https://github.com/jenly1314/AppUpdater/tree/androidx)
* 支持添加请求头
#### v1.0.3:2019-5-9
@ -173,10 +203,15 @@ AppUpdater for Android 是一个专注于App更新,一键傻瓜式集成App版
CSDN: <a title="CSDN博客" href="http://blog.csdn.net/jenly121" target="_blank">jenly121</a>
Github: <a title="Github开源项目" href="https://github.com/jenly1314" target="_blank">jenly1314</a>
CNBlogs: <a title="博客园" href="https://www.cnblogs.com/jenly" target="_blank">jenly</a>
GitHub: <a title="GitHub开源项目" href="https://github.com/jenly1314" target="_blank">jenly1314</a>
Gitee: <a title="Gitee开源项目" href="https://gitee.com/jenly1314" target="_blank">jenly1314</a>
加入QQ群: <a title="点击加入QQ群" href="http://shang.qq.com/wpa/qunwpa?idkey=8fcc6a2f88552ea44b1411582c94fd124f7bb3ec227e2a400dbbfaad3dc2f5ad" target="_blank">20867961</a>
<div>
<img src="https://jenly1314.github.io/image/jenly666.png">
<img src="https://jenly1314.github.io/image/qqgourp.png">
</div>

@ -1,5 +1,6 @@
apply plugin: 'com.android.library'
apply from: 'bintray.gradle'
//apply from: 'bintray.gradle'
apply plugin: "com.vanniktech.maven.publish"
android {
@ -45,3 +46,4 @@ dependencies {

@ -0,0 +1,3 @@
POM_NAME=App Dialog
POM_ARTIFACT_ID=app-dialog
POM_PACKAGING=aar

@ -19,3 +19,11 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn com.king.app.dialog.**
-keep class com.king.app.dialog.**{ *;}
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}

@ -7,13 +7,10 @@ import android.support.annotation.NonNull;
import android.support.annotation.StyleRes;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.FragmentManager;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.TextView;
import com.king.app.dialog.fragment.AppDialogFragment;
@ -33,59 +30,31 @@ public enum AppDialog {
//-------------------------------------------
/**
* 通过{@link AppDialogConfig} 创建一个视图
* @param context
* @param config 弹框配置 {@link AppDialogConfig}
* @return
* @deprecated 即将废弃下一个版本可能会移除此方法
*/
@Deprecated
public View createAppDialogView(@NonNull Context context,@NonNull AppDialogConfig config){
View view = config.getView(context);
TextView tvDialogTitle = view.findViewById(config.getTitleId());
setText(tvDialogTitle,config.getTitle());
tvDialogTitle.setVisibility(config.isHideTitle() ? View.GONE : View.VISIBLE);
TextView tvDialogContent = view.findViewById(config.getContentId());
setText(tvDialogContent,config.getContent());
Button btnDialogCancel = view.findViewById(config.getCancelId());
setText(btnDialogCancel,config.getCancel());
btnDialogCancel.setOnClickListener(config.getOnClickCancel() != null ? config.getOnClickCancel() : mOnClickDismissDialog);
btnDialogCancel.setVisibility(config.isHideCancel() ? View.GONE : View.VISIBLE);
try{
//不强制要求要有中间的线
View line = view.findViewById(R.id.line);
line.setVisibility(config.isHideCancel() ? View.GONE : View.VISIBLE);
}catch (Exception e){
}
Button btnDialogOK = view.findViewById(config.getOkId());
setText(btnDialogOK,config.getOk());
btnDialogOK.setOnClickListener(config.getOnClickOk() != null ? config.getOnClickOk() : mOnClickDismissDialog);
return view;
return config.buildAppDialogView();
}
//-------------------------------------------
private View.OnClickListener mOnClickDismissDialog = new View.OnClickListener() {
View.OnClickListener mOnClickDismissDialog = new View.OnClickListener() {
@Override
public void onClick(View v) {
dismissDialog();
}
};
private void setText(TextView tv,CharSequence text){
if(!TextUtils.isEmpty(text)){
tv.setText(text);
}
}
//-------------------------------------------
public void dismissDialogFragment(FragmentManager fragmentManager){
dismissDialogFragment(fragmentManager,mTag);
mTag = null;
}
public void dismissDialogFragment(FragmentManager fragmentManager,String tag){
@ -147,23 +116,45 @@ public enum AppDialog {
/**
* 显示弹框
* @param config 弹框配置 {@link AppDialogConfig}
*/
public void showDialog(AppDialogConfig config){
showDialog(config,true);
}
/**
* 显示弹框 请使用{@link #showDialog(AppDialogConfig)}
* @param context
* @param config 弹框配置 {@link AppDialogConfig}
* @deprecated 即将废弃下一个版本可能会移除此方法
*/
@Deprecated
public void showDialog(Context context,AppDialogConfig config){
showDialog(context,config,true);
showDialog(config,true);
}
/**
* 显示弹框
* 显示弹框请使用{@link #showDialog(AppDialogConfig, boolean)}
* @param context
* @param config 弹框配置 {@link AppDialogConfig}
* @param isCancel 是否可取消默认为truefalse则拦截back键
* @deprecated 即将废弃下一个版本可能会移除此方法
*/
@Deprecated
public void showDialog(Context context,AppDialogConfig config,boolean isCancel){
showDialog(context,createAppDialogView(context,config),R.style.app_dialog,DEFAULT_WIDTH_RATIO,isCancel);
showDialog(config,isCancel);
}
/**
* 显示弹框
* @param config 弹框配置 {@link AppDialogConfig}
* @param isCancel 是否可取消默认为truefalse则拦截back键
*/
public void showDialog(AppDialogConfig config,boolean isCancel){
showDialog(config.getContext(),config.buildAppDialogView(),config.getStyleId(),DEFAULT_WIDTH_RATIO,isCancel);
}
/**
* 显示弹框
* @param context
@ -208,40 +199,33 @@ public enum AppDialog {
* 显示弹框
* @param context
* @param contentView 弹框内容视图
* @param resId Dialog样式
* @param styleId Dialog样式
* @param widthRatio 宽度比例根据屏幕宽度计算得来
*/
public void showDialog(Context context, View contentView, @StyleRes int resId, float widthRatio){
showDialog(context,contentView,resId,widthRatio,true);
public void showDialog(Context context, View contentView, @StyleRes int styleId, float widthRatio){
showDialog(context,contentView,styleId,widthRatio,true);
}
/**
* 显示弹框
* @param context
* @param contentView 弹框内容视图
* @param resId Dialog样式
* @param styleId Dialog样式
* @param widthRatio 宽度比例根据屏幕宽度计算得来
* @param isCancel 是否可取消默认为truefalse则拦截back键
*/
public void showDialog(Context context, View contentView, @StyleRes int resId, float widthRatio,final boolean isCancel){
public void showDialog(Context context, View contentView, @StyleRes int styleId, float widthRatio,final boolean isCancel){
dismissDialog();
mDialog = new Dialog(context,resId);
mDialog.setContentView(contentView);
mDialog.setCanceledOnTouchOutside(false);
mDialog.setOnKeyListener(new DialogInterface.OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
if(keyCode == KeyEvent.KEYCODE_BACK && isCancel){
dismissDialog();
}
return true;
}
});
setDialogWindow(context,mDialog,widthRatio);
mDialog = createDialog(context,contentView,styleId,widthRatio,isCancel);
mDialog.show();
}
/**
* 设置弹框窗口配置
* @param context
* @param dialog
* @param widthRatio
*/
private void setDialogWindow(Context context,Dialog dialog,float widthRatio){
Window window = dialog.getWindow();
WindowManager.LayoutParams lp = window.getAttributes();
@ -249,16 +233,141 @@ public enum AppDialog {
window.setAttributes(lp);
}
/**
* 创建弹框
* @param config 弹框配置 {@link AppDialogConfig}
*/
public Dialog createDialog(AppDialogConfig config){
return createDialog(config,true);
}
/**
* 创建弹框请使用{@link #createDialog(AppDialogConfig)}
* @param context
* @param config 弹框配置 {@link AppDialogConfig}
* @deprecated 即将废弃下一个版本可能会移除此方法
*/
@Deprecated
public Dialog createDialog(Context context,AppDialogConfig config){
return createDialog(config,true);
}
/**
* 创建弹框请使用{@link #createDialog(AppDialogConfig, boolean)}
* @param context
* @param config 弹框配置 {@link AppDialogConfig}
* @param isCancel 是否可取消默认为truefalse则拦截back键
* @deprecated 即将废弃下一个版本可能会移除此方法
*/
@Deprecated
public Dialog createDialog(Context context,AppDialogConfig config,boolean isCancel){
return createDialog(config,isCancel);
}
/**
* 创建弹框
* @param config 弹框配置 {@link AppDialogConfig}
* @param isCancel 是否可取消默认为truefalse则拦截back键
*/
public Dialog createDialog(AppDialogConfig config,boolean isCancel){
return createDialog(config.getContext(),config.buildAppDialogView(),config.getStyleId(),DEFAULT_WIDTH_RATIO,isCancel);
}
/**
* 创建弹框
* @param context
* @param contentView 弹框内容视图
*/
public Dialog createDialog(Context context,View contentView){
return createDialog(context,contentView,DEFAULT_WIDTH_RATIO);
}
/**
* 创建弹框
* @param context
* @param contentView 弹框内容视图
* @param isCancel 是否可取消默认为truefalse则拦截back键
*/
public Dialog createDialog(Context context,View contentView,boolean isCancel){
return createDialog(context,contentView,R.style.app_dialog,DEFAULT_WIDTH_RATIO,isCancel);
}
/**
* 创建弹框
* @param context
* @param contentView 弹框内容视图
* @param widthRatio 宽度比例根据屏幕宽度计算得来
*/
public Dialog createDialog(Context context,View contentView,float widthRatio){
return createDialog(context,contentView,widthRatio,true);
}
/**
* 创建弹框
* @param context
* @param contentView 弹框内容视图
* @param widthRatio 宽度比例根据屏幕宽度计算得来
* @param isCancel 是否可取消默认为truefalse则拦截back键
*/
public Dialog createDialog(Context context,View contentView,float widthRatio,boolean isCancel){
return createDialog(context,contentView,R.style.app_dialog,widthRatio,isCancel);
}
/**
* 创建弹框
* @param context
* @param contentView 弹框内容视图
* @param styleId Dialog样式
* @param widthRatio 宽度比例根据屏幕宽度计算得来
*/
public Dialog createDialog(Context context, View contentView, @StyleRes int styleId, float widthRatio){
return createDialog(context,contentView,styleId,widthRatio,true);
}
/**
* 创建弹框
* @param context
* @param contentView 弹框内容视图
* @param styleId Dialog样式
* @param widthRatio 宽度比例根据屏幕宽度计算得来
* @param isCancel 是否可取消默认为truefalse则拦截back键
*/
public Dialog createDialog(Context context, View contentView, @StyleRes int styleId, float widthRatio,final boolean isCancel){
Dialog dialog = new Dialog(context,styleId);
dialog.setContentView(contentView);
dialog.setCanceledOnTouchOutside(false);
dialog.setOnKeyListener(new DialogInterface.OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
if(keyCode == KeyEvent.KEYCODE_BACK){
if(isCancel){
dismissDialog();
}
return true;
}
return false;
}
});
setDialogWindow(context,dialog,widthRatio);
return dialog;
}
public Dialog getDialog(){
return mDialog;
}
public void dismissDialog(){
dismissDialog(mDialog);
mDialog = null;
}
private void dismissDialog(Dialog dialog){
if(dialog!=null){
public void dismissDialog(Dialog dialog){
if(dialog != null){
dialog.dismiss();
}
}
//-------------------------------------------
}
}

@ -4,201 +4,114 @@ import android.content.Context;
import android.support.annotation.IdRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
/**
* @author Jenly <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
public class AppDialogConfig {
/**
* 布局ID
*/
private @LayoutRes int layoutId = R.layout.app_dialog;
/**
* 标题视图ID
*/
private @IdRes int titleId = R.id.tvDialogTitle;
/**
* 内容视图ID
*/
private @IdRes int contentId = R.id.tvDialogContent;
/**
* 取消视图ID左边按钮
*/
private @IdRes int cancelId = R.id.btnDialogCancel;
/**
* 确定视图ID右边按钮
*/
private @IdRes int okId = R.id.btnDialogOK;
/**
* 按钮中间分割线ID
*/
private @IdRes int line = R.id.line;
/**
* 标题文本
*/
private CharSequence title;
/**
* 内容文本
*/
private CharSequence content;
/**
* 取消按钮文本
*/
private CharSequence cancel;
/**
* 确定按钮文本
*/
private CharSequence ok;
/**
* 是否隐藏取消按钮如果隐藏取消则底部只显示一个按钮
*/
private boolean isHideCancel;
/**
* 是否隐藏标题
*/
private boolean isHideTitle;
public class AppDialogConfig extends BaseDialogConfig{
private View.OnClickListener onClickCancel;
private Context context;
private View.OnClickListener onClickOk;
private SparseArray<View> views;
private View view;
public @LayoutRes int getLayoutId() {
return layoutId;
}
public AppDialogConfig setLayoutId(@LayoutRes int layoutId) {
this.layoutId = layoutId;
return this;
}
public int getTitleId() {
return titleId;
}
public AppDialogConfig setTitleId(@IdRes int titleId) {
this.titleId = titleId;
return this;
}
public @IdRes int getContentId() {
return contentId;
}
public AppDialogConfig setContentId(@IdRes int contentId) {
this.contentId = contentId;
return this;
}
public @IdRes int getCancelId() {
return cancelId;
}
public AppDialogConfig setCancelId(@IdRes int cancelId) {
this.cancelId = cancelId;
return this;
}
public @IdRes int getOkId() {
return okId;
}
public AppDialogConfig setOkId(@IdRes int okId) {
this.okId = okId;
return this;
public AppDialogConfig(@NonNull Context context){
this(context,R.layout.app_dialog);
}
public @IdRes int getLine() {
return line;
public AppDialogConfig(@NonNull Context context,@LayoutRes int layoutId){
super(layoutId);
this.context = context;
views = new SparseArray<>();
}
public AppDialogConfig setLine(@IdRes int line) {
this.line = line;
return this;
public Context getContext(){
return context;
}
public CharSequence getTitle() {
return title;
}
public AppDialogConfig setTitle(CharSequence title) {
this.title = title;
return this;
}
public CharSequence getContent() {
return content;
}
public AppDialogConfig setContent(CharSequence content) {
this.content = content;
return this;
/**
* use {@link #getDialogView()}
* @param context
* @return
* @deprecated 即将废弃下一个版本可能会移除此方法
*/
@Deprecated
public View getView(@NonNull Context context){
return getDialogView();
}
public CharSequence getCancel() {
return cancel;
public View getDialogView(){
if(view == null){
view = LayoutInflater.from(context).inflate(getLayoutId(),null);
}
return view;
}
public AppDialogConfig setCancel(CharSequence cancel) {
this.cancel = cancel;
return this;
private <T extends View> T findView(@IdRes int id){
return (T)getDialogView().findViewById(id);
}
public CharSequence getOk() {
return ok;
}
public <T extends View> T getView(@IdRes int id){
View v = views.get(id);
if(v == null){
v = findView(id);
views.put(id,v);
}
public AppDialogConfig setOk(CharSequence ok) {
this.ok = ok;
return this;
return (T)v;
}
public boolean isHideCancel() {
return isHideCancel;
}
public AppDialogConfig setHideCancel(boolean hideCancel) {
isHideCancel = hideCancel;
return this;
}
/**
* 通过{@link AppDialogConfig} 创建一个视图
* @return
*/
View buildAppDialogView(){
TextView tvDialogTitle = getView(titleId);
if(tvDialogTitle != null){
setText(tvDialogTitle,title);
tvDialogTitle.setVisibility(isHideTitle ? View.GONE : View.VISIBLE);
}
public boolean isHideTitle(){
return isHideTitle;
}
TextView tvDialogContent = getView(contentId);
if(tvDialogContent != null){
setText(tvDialogContent,content);
}
public AppDialogConfig setHideTitle(boolean hideTitle){
isHideTitle = hideTitle;
return this;
}
Button btnDialogCancel = getView(cancelId);
if(btnDialogCancel != null){
setText(btnDialogCancel,cancel);
btnDialogCancel.setOnClickListener(onClickCancel != null ? onClickCancel : AppDialog.INSTANCE.mOnClickDismissDialog);
btnDialogCancel.setVisibility(isHideCancel ? View.GONE : View.VISIBLE);
}
public View.OnClickListener getOnClickCancel() {
return onClickCancel;
}
View line = getView(lineId);
if(line != null){
line.setVisibility(isHideCancel ? View.GONE : View.VISIBLE);
}
public AppDialogConfig setOnClickCancel(View.OnClickListener onClickCancel) {
this.onClickCancel = onClickCancel;
return this;
}
Button btnDialogOK = getView(okId);
if(btnDialogOK != null){
setText(btnDialogOK,ok);
btnDialogOK.setOnClickListener(onClickOk != null ? onClickOk : AppDialog.INSTANCE.mOnClickDismissDialog);
public View.OnClickListener getOnClickOk() {
return onClickOk;
}
}
public AppDialogConfig setOnClickOk(View.OnClickListener onClickOk) {
this.onClickOk = onClickOk;
return this;
return view;
}
public View getView(@NonNull Context context){
if(view == null){
view = LayoutInflater.from(context).inflate(layoutId,null);
private void setText(TextView tv,CharSequence text){
if(text != null){
tv.setText(text);
}
return view;
}
}

@ -0,0 +1,251 @@
package com.king.app.dialog;
import android.content.Context;
import android.support.annotation.IdRes;
import android.support.annotation.LayoutRes;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.support.annotation.StyleRes;
import android.view.View;
/**
* @author Jenly <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
public class BaseDialogConfig {
/**
* 布局ID
*/
@LayoutRes int layoutId;
/**
* 标题视图ID
*/
@IdRes int titleId = R.id.tvDialogTitle;
/**
* 内容视图ID
*/
@IdRes int contentId = R.id.tvDialogContent;
/**
* 取消视图ID左边按钮
*/
@IdRes int cancelId = R.id.btnDialogCancel;
/**
* 确定视图ID右边按钮
*/
@IdRes int okId = R.id.btnDialogOK;
/**
* 按钮中间分割线ID
*/
@IdRes int lineId = R.id.line;
/**
* 样式ID
*/
@StyleRes int styleId = R.style.app_dialog;
/**
* 标题文本
*/
CharSequence title;
/**
* 内容文本
*/
CharSequence content;
/**
* 取消按钮文本
*/
CharSequence cancel;
/**
* 确定按钮文本
*/
CharSequence ok;
/**
* 是否隐藏取消按钮如果隐藏取消则底部只显示一个按钮
*/
boolean isHideCancel;
/**
* 是否隐藏标题
*/
boolean isHideTitle;
View.OnClickListener onClickCancel;
View.OnClickListener onClickOk;
public BaseDialogConfig(){
this(R.layout.app_dialog);
}
public BaseDialogConfig(@LayoutRes int layoutId){
this.layoutId = layoutId;
}
public @LayoutRes int getLayoutId() {
return layoutId;
}
/**
* @param layoutId
* @return
* @deprecated 即将废弃下一个版本可能会移除此方法
*/
@Deprecated
public BaseDialogConfig setLayoutId(@IdRes int layoutId) {
this.layoutId = layoutId;
return this;
}
public int getTitleId() {
return titleId;
}
public BaseDialogConfig setTitleId(@IdRes int titleId) {
this.titleId = titleId;
return this;
}
public int getStyleId() {
return styleId;
}
public BaseDialogConfig setStyleId(@IdRes int styleId) {
this.styleId = styleId;
return this;
}
public @IdRes int getContentId() {
return contentId;
}
public BaseDialogConfig setContentId(@IdRes int contentId) {
this.contentId = contentId;
return this;
}
public @IdRes int getCancelId() {
return cancelId;
}
public BaseDialogConfig setCancelId(@IdRes int cancelId) {
this.cancelId = cancelId;
return this;
}
public @IdRes int getOkId() {
return okId;
}
public BaseDialogConfig setOkId(@IdRes int okId) {
this.okId = okId;
return this;
}
public @IdRes int getLineId() {
return lineId;
}
public BaseDialogConfig setLineId(@IdRes int lineId) {
this.lineId = lineId;
return this;
}
public CharSequence getTitle() {
return title;
}
public BaseDialogConfig setTitle(CharSequence title) {
this.title = title;
return this;
}
public BaseDialogConfig setTitle(@NonNull Context context, @StringRes int resId) {
this.title = context.getString(resId);
return this;
}
public CharSequence getContent() {
return content;
}
public BaseDialogConfig setContent(CharSequence content) {
this.content = content;
return this;
}
public CharSequence getCancel() {
return cancel;
}
public BaseDialogConfig setCancel(CharSequence cancel) {
this.cancel = cancel;
return this;
}
public BaseDialogConfig setCancel(@NonNull Context context, @StringRes int resId) {
this.cancel = context.getString(resId);
return this;
}
public CharSequence getOk() {
return ok;
}
public BaseDialogConfig setOk(CharSequence ok) {
this.ok = ok;
return this;
}
public BaseDialogConfig setOk(@NonNull Context context, @StringRes int resId) {
this.ok = context.getString(resId);
return this;
}
public boolean isHideCancel() {
return isHideCancel;
}
public BaseDialogConfig setHideCancel(boolean hideCancel) {
isHideCancel = hideCancel;
return this;
}
public boolean isHideTitle(){
return isHideTitle;
}
public BaseDialogConfig setHideTitle(boolean hideTitle){
isHideTitle = hideTitle;
return this;
}
public View.OnClickListener getOnClickCancel() {
return onClickCancel;
}
/**
* 设置取消按钮点击监听不设置默认点击关闭弹框
* @param onClickCancel
* @return
*/
public BaseDialogConfig setOnClickCancel(View.OnClickListener onClickCancel) {
this.onClickCancel = onClickCancel;
return this;
}
public View.OnClickListener getOnClickOk() {
return onClickOk;
}
/**
* 设置确定按钮点击监听不设置默认点击关闭弹框
* @param onClickOk
* @return
*/
public BaseDialogConfig setOnClickOk(View.OnClickListener onClickOk) {
this.onClickOk = onClickOk;
return this;
}
}

@ -5,7 +5,7 @@ import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import com.king.app.dialog.AppDialogConfig;
import com.king.app.dialog.BaseDialogConfig;
import com.king.app.dialog.R;
/**
@ -13,9 +13,9 @@ import com.king.app.dialog.R;
*/
public class AppDialogFragment extends BaseDialogFragment {
private AppDialogConfig config;
private BaseDialogConfig config;
public static AppDialogFragment newInstance(AppDialogConfig config) {
public static AppDialogFragment newInstance(BaseDialogConfig config) {
Bundle args = new Bundle();
AppDialogFragment fragment = new AppDialogFragment();
@ -26,37 +26,42 @@ public class AppDialogFragment extends BaseDialogFragment {
@Override
public int getRootLayoutId() {
if(config == null){
config = new AppDialogConfig();
if(config != null){
return config.getLayoutId();
}
return config.getLayoutId();
return R.layout.app_dialog;
}
public void init(View rootView){
if(config!=null){
if(config != null){
TextView tvDialogTitle = rootView.findViewById(config.getTitleId());
setText(tvDialogTitle,config.getTitle());
tvDialogTitle.setVisibility(config.isHideTitle() ? View.GONE : View.VISIBLE);
if(tvDialogTitle != null){
setText(tvDialogTitle,config.getTitle());
tvDialogTitle.setVisibility(config.isHideTitle() ? View.GONE : View.VISIBLE);
}
TextView tvDialogContent = rootView.findViewById(config.getContentId());
setText(tvDialogContent,config.getContent());
if(tvDialogContent != null){
setText(tvDialogContent,config.getContent());
}
Button btnDialogCancel = rootView.findViewById(config.getCancelId());
setText(btnDialogCancel,config.getCancel());
btnDialogCancel.setOnClickListener(config.getOnClickCancel() != null ? config.getOnClickCancel() : getOnClickDismiss());
btnDialogCancel.setVisibility(config.isHideCancel() ? View.GONE : View.VISIBLE);
if(btnDialogCancel != null){
setText(btnDialogCancel,config.getCancel());
btnDialogCancel.setOnClickListener(config.getOnClickCancel() != null ? config.getOnClickCancel() : getOnClickDismiss());
btnDialogCancel.setVisibility(config.isHideCancel() ? View.GONE : View.VISIBLE);
}
try{
//不强制要求要有中间的线
View line = rootView.findViewById(R.id.line);
View line = rootView.findViewById(config.getLineId());
if(line != null){
line.setVisibility(config.isHideCancel() ? View.GONE : View.VISIBLE);
}catch (Exception e){
}
Button btnDialogOK = rootView.findViewById(config.getOkId());
setText(btnDialogOK,config.getOk());
btnDialogOK.setOnClickListener(config.getOnClickOk() != null ? config.getOnClickOk() : getOnClickDismiss());
if(btnDialogOK != null){
setText(btnDialogOK,config.getOk());
btnDialogOK.setOnClickListener(config.getOnClickOk() != null ? config.getOnClickOk() : getOnClickDismiss());
}
}
}

@ -8,7 +8,6 @@ import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.DialogFragment;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -58,7 +57,7 @@ public abstract class BaseDialogFragment extends DialogFragment {
}
protected void setText(TextView tv, CharSequence text){
if(!TextUtils.isEmpty(text)){
if(text != null){
tv.setText(text);
}
}

@ -11,8 +11,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:minHeight="40dp"
android:padding="6dp"
android:padding="10dp"
android:lines="1"
android:textSize="@dimen/app_dialog_title_text_size"
android:textColor="@color/app_dialog_title_color"
@ -24,9 +23,8 @@
android:padding="10dp"
android:textSize="@dimen/app_dialog_content_text_size"
android:textColor="@color/app_dialog_content_color"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:lineSpacingMultiplier="1" />
android:lineSpacingMultiplier="1.2" />
<include layout="@layout/app_dialog_line_h"/>
<LinearLayout
android:orientation="horizontal"

@ -8,6 +8,6 @@
<color name="app_dialog_button_normal_color">#333333</color>
<color name="app_dialog_button_pressed_color">@color/colorAccent</color>
<color name="app_dialog_line_color">#d2d2d2</color>
<color name="app_dialog_line_color">#f2f2f2</color>
</resources>

@ -6,5 +6,5 @@
<dimen name="app_dialog_title_text_size">16sp</dimen>
<dimen name="app_dialog_content_text_size">16sp</dimen>
<dimen name="app_dialog_button_text_size">18sp</dimen>
<dimen name="app_dialog_button_text_size">16sp</dimen>
</resources>

@ -3,5 +3,5 @@
<string name="app_dialog_title">提示</string>
<string name="app_dialog_cancel">取消</string>
<string name="app_dialog_ok"></string>
<string name="app_dialog_ok"></string>
</resources>

@ -1,5 +1,6 @@
apply plugin: 'com.android.library'
apply from: 'bintray.gradle'
//apply from: 'bintray.gradle'
apply plugin: "com.vanniktech.maven.publish"
android {
@ -36,5 +37,6 @@ dependencies {
androidTestImplementation deps.test.espresso
//support
compileOnly deps.support.appcompat
compileOnly deps.okhttp
}

@ -0,0 +1,3 @@
POM_NAME=App Updater
POM_ARTIFACT_ID=app-updater
POM_PACKAGING=aar

@ -19,3 +19,13 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-dontwarn com.king.app.updater.**
-keep class com.king.app.updater.**{ *;}
-keep class * extends com.king.app.updater.**{ *;}
-keep class * implements com.king.app.updater.**{ *;}
-keepattributes InnerClasses
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}

@ -11,10 +11,10 @@
<application>
<service android:name=".service.DownloadService"/>
<service android:name="com.king.app.updater.service.DownloadService"/>
<provider
android:name=".provider.AppUpdaterFileProvider"
android:name="com.king.app.updater.provider.AppUpdaterFileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">

@ -13,7 +13,9 @@ import android.util.Log;
import com.king.app.updater.callback.UpdateCallback;
import com.king.app.updater.constant.Constants;
import com.king.app.updater.http.HttpManager;
import com.king.app.updater.http.IHttpManager;
import com.king.app.updater.http.OkHttpManager;
import com.king.app.updater.service.DownloadService;
import com.king.app.updater.util.PermissionUtils;
@ -54,11 +56,24 @@ public class AppUpdater {
mConfig.setUrl(url);
}
/**
* 设置下载更新进度回调
* @param callback
* @return
*/
public AppUpdater setUpdateCallback(UpdateCallback callback){
this.mCallback = callback;
return this;
}
/**
* 设置一个IHttpManager
* @param httpManager AppUpdater内置提供{@link HttpManager} {@link OkHttpManager}两种实现
* 如果不设置将默认使用{@link HttpManager},你也可以使用{@link OkHttpManager}或自己去实现一个
* {@link IHttpManager}
* 当使用{@link OkHttpManager}必需依赖okhttp库
* @return
*/
public AppUpdater setHttpManager(IHttpManager httpManager){
this.mHttpManager = httpManager;
return this;
@ -69,12 +84,12 @@ public class AppUpdater {
*/
public void start(){
if(mConfig!=null && !TextUtils.isEmpty(mConfig.getUrl())){
//如果mContext是Activity,则默认会校验一次动态权限。
if(mContext instanceof Activity){
PermissionUtils.INSTANCE.verifyReadAndWritePermissions((Activity) mContext,Constants.RE_CODE_STORAGE_PERMISSION);
//如果mContext是Activity,并且配置了下载路径,则默认会校验一次动态权限。
if(mContext instanceof Activity && !TextUtils.isEmpty(mConfig.getPath())){
PermissionUtils.verifyReadAndWritePermissions((Activity) mContext,Constants.RE_CODE_STORAGE_PERMISSION);
}
if(mConfig.isShowNotification() && !PermissionUtils.INSTANCE.isNotificationEnabled(mContext)){
if(mConfig.isShowNotification() && !PermissionUtils.isNotificationEnabled(mContext)){
Log.w(Constants.TAG,"Notification permission not enabled.");
}
@ -111,9 +126,24 @@ public class AppUpdater {
}
}
/**
* 取消下载
*/
public void stop(){
stopDownloadService();
}
/**
* AppUpdater构建器
* 停止下载服务
*/
private void stopDownloadService(){
Intent intent = new Intent(mContext, DownloadService.class);
intent.putExtra(Constants.KEY_STOP_DOWNLOAD_SERVICE,true);
mContext.startService(intent);
}
/**
* AppUpdater建造者
*/
public static class Builder{
@ -128,16 +158,17 @@ public class AppUpdater {
* @param url 下载地址
* @return
*/
public Builder serUrl(@NonNull String url){
public Builder setUrl(@NonNull String url){
mConfig.setUrl(url);
return this;
}
/**
* 设置保存的路径
* @param path 下载保存的文件路径 默认SD卡/.AppUpdater目录
* 设置保存的路径建议使用默认不做设置
* @param path 下载保存的文件路径
* @return
*/
@Deprecated
public Builder setPath(String path){
mConfig.setPath(path);
return this;
@ -236,7 +267,7 @@ public class AppUpdater {
/**
* 设置FileProvider的authority
* @param authority FileProvider的authority默认兼容N默认值context.getPackageName() + ".fileProvider"
* @param authority FileProvider的authority默认兼容N默认值{@link Context#getPackageName() + ".fileProvider"}
* @return
*/
public Builder setAuthority(String authority){
@ -255,8 +286,8 @@ public class AppUpdater {
}
/**
* 设置下载失败是是否支持点击通知栏重新下载
* @param reDownload 下载失败时是否支持点击通知栏重新下载默认true 最多重新下载3次
* 设置下载失败是否支持点击通知栏重新下载与之相关联的方法{@link #setReDownloads(int)}
* @param reDownload 下载失败时是否支持点击通知栏重新下载默认true
* @return
*/
public Builder setReDownload(boolean reDownload) {
@ -265,7 +296,19 @@ public class AppUpdater {
}
/**
* 设置要下载APK的versionCode
* 设置下载失败时最多重新下载次数与之相关联的方法{@link #setReDownload(boolean)}
* @param reDownloads 下载失败时是否支持点击通知栏重新下载默认最多重新下载3次
* @return
*/
public Builder setReDownloads(int reDownloads) {
mConfig.setReDownloads(reDownloads);
return this;
}
/**
* 设置要下载APK的versionCode用于优先取缓存时通过versionCode校验APK文件是否一致
* 缓存校验目前支持两种方式一种是通过versionCode校验{@link #setVersionCode(Integer)}一种是文件MD5校验{@link #setApkMD5(String)}推荐使用MD5校验方式
* 如果两种方式都设置了则只校验MD5
* @param versionCode 为null表示不处理默认不存在则下载存在则重新下载不为null时表示会优先校验本地是否存在已下载版本号为versionCode的APK
* 如果存在则不会重新下载(AppUpdater会自动校验packageName一致性)直接取本地APK反之重新下载
* @return
@ -275,6 +318,17 @@ public class AppUpdater {
return this;
}
/**
* 设置APK文件的MD5用于优先取缓存时通过MD5校验文件APK是否一致
* 缓存校验目前支持两种方式一种是通过versionCode校验{@link #setVersionCode(Integer)}一种是文件MD5校验{@link #setApkMD5(String)}推荐使用MD5校验方式
* 如果两种方式都设置了则只校验MD5
* @param md5 为null表示不处理如果设置了MD5则缓存APK的MD5相同时只下载一次优先取本地缓存
* @return
*/
public Builder setApkMD5(String md5) {
mConfig.setApkMD5(md5);
return this;
}
/**
* 请求头添加参数
* @param key
@ -296,6 +350,16 @@ public class AppUpdater {
return this;
}
/**
* 设置是否自动删除取消下载的文件
* @param isDeleteCancelFile 是否删除取消下载的文件默认为true
* @return
*/
public Builder setDeleteCancelFile(boolean isDeleteCancelFile){
mConfig.setDeleteCancelFile(isDeleteCancelFile);
return this;
}
public AppUpdater build(@NonNull Context context){
AppUpdater appUpdater = new AppUpdater(context,mConfig);
return appUpdater;

@ -59,9 +59,13 @@ public class UpdateConfig implements Parcelable {
*/
private String mAuthority;
/**
* 下载失败是否支持点击通知栏重下载
* 下载失败是否支持点击通知栏重下载
*/
private boolean isReDownload = true;
/**
* 下载失败后最大重新下载次数
*/
private int reDownloads = 3;
/**
* 是否显示百分比
*/
@ -87,6 +91,16 @@ public class UpdateConfig implements Parcelable {
*/
private Map<String,String> mRequestProperty;
/**
* 是否删除取消下载的文件
*/
private boolean isDeleteCancelFile = true;
/**
* APK文件的MD5
*/
private String apkMD5;
public UpdateConfig() {
@ -188,6 +202,14 @@ public class UpdateConfig implements Parcelable {
isReDownload = reDownload;
}
public int getReDownloads() {
return reDownloads;
}
public void setReDownloads(int reDownloads) {
this.reDownloads = reDownloads;
}
public boolean isVibrate() {
return isVibrate;
}
@ -216,6 +238,15 @@ public class UpdateConfig implements Parcelable {
return mRequestProperty;
}
public void setApkMD5(String md5){
this.apkMD5 = md5;
}
public String getApkMD5(){
return apkMD5;
}
public void addHeader(String key, String value){
initRequestProperty();
mRequestProperty.put(key,value);
@ -232,6 +263,14 @@ public class UpdateConfig implements Parcelable {
}
}
public boolean isDeleteCancelFile() {
return isDeleteCancelFile;
}
public void setDeleteCancelFile(boolean deleteCancelFile) {
isDeleteCancelFile = deleteCancelFile;
}
@Override
public int describeContents() {
@ -251,18 +290,23 @@ public class UpdateConfig implements Parcelable {
dest.writeString(this.mChannelName);
dest.writeString(this.mAuthority);
dest.writeByte(this.isReDownload ? (byte) 1 : (byte) 0);
dest.writeInt(this.reDownloads);
dest.writeByte(this.isShowPercentage ? (byte) 1 : (byte) 0);
dest.writeByte(this.isVibrate ? (byte) 1 : (byte) 0);
dest.writeByte(this.isSound ? (byte) 1 : (byte) 0);
dest.writeValue(this.versionCode);
dest.writeInt(mRequestProperty!=null ? this.mRequestProperty.size():0);
if(mRequestProperty!=null){
dest.writeInt(this.mRequestProperty.size());
for (Map.Entry<String, String> entry : this.mRequestProperty.entrySet()) {
dest.writeString(entry.getKey());
dest.writeString(entry.getValue());
}
}else{
dest.writeInt(0);
}
dest.writeByte(this.isDeleteCancelFile ? (byte) 1 : (byte) 0);
dest.writeString(this.apkMD5);
}
protected UpdateConfig(Parcel in) {
@ -277,6 +321,7 @@ public class UpdateConfig implements Parcelable {
this.mChannelName = in.readString();
this.mAuthority = in.readString();
this.isReDownload = in.readByte() != 0;
this.reDownloads = in.readInt();
this.isShowPercentage = in.readByte() != 0;
this.isVibrate = in.readByte() != 0;
this.isSound = in.readByte() != 0;
@ -288,6 +333,8 @@ public class UpdateConfig implements Parcelable {
String value = in.readString();
this.mRequestProperty.put(key, value);
}
this.isDeleteCancelFile = in.readByte() != 0;
this.apkMD5 = in.readString();
}
public static final Creator<UpdateConfig> CREATOR = new Creator<UpdateConfig>() {

@ -24,7 +24,7 @@ public interface UpdateCallback {
* @param total
* @param isChange 进度百分比是否有改变主要可以用来过滤无用的刷新从而降低刷新频率
*/
void onProgress(int progress,int total,boolean isChange);
void onProgress(long progress,long total,boolean isChange);
/**
* 完成

@ -1,9 +1,5 @@
package com.king.app.updater.constant;
import android.os.Environment;
import java.io.File;
/**
* @author Jenly <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
@ -23,9 +19,12 @@ public final class Constants {
public static final String KEY_RE_DOWNLOAD = "app_update_re_download";
public static final String DEFAULT_DIR_PATH = Environment.getExternalStorageDirectory() + File.separator + ".AppUpdater";
public static final int RE_CODE_STORAGE_PERMISSION = 0x66;
public static final int NONE = -1;
public static final String DEFAULT_FILE_PROVIDER = ".fileProvider";
public static final String DEFAULT_DIR = "apk";
}

@ -19,20 +19,28 @@ import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
/**
* HttpManager使用{@link HttpURLConnection}实现{@link IHttpManager}
* @author Jenly <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
public class HttpManager implements IHttpManager {
private static final int HTTP_TEMP_REDIRECT = 307;
private static final int HTTP_PERM_REDIRECT = 308;
private static final int DEFAULT_TIME_OUT = 20000;
private int mTimeout;
private static HttpManager INSTANCE;
private boolean isCancel;
private static volatile HttpManager INSTANCE;
public static HttpManager getInstance(){
if(INSTANCE == null){
synchronized (HttpManager.class){
INSTANCE = new HttpManager();
if(INSTANCE == null){
INSTANCE = new HttpManager();
}
}
}
@ -43,20 +51,29 @@ public class HttpManager implements IHttpManager {
this(DEFAULT_TIME_OUT);
}
/**
* HttpManager对外暴露如果没有特殊需求推荐使用{@link HttpManager#getInstance()}
*/
public HttpManager(int timeout){
this.mTimeout = timeout;
}
@Override
public void download(String url, String path, String filename, @Nullable Map<String,String> requestProperty, DownloadCallback callback) {
isCancel = false;
new DownloadTask(url,path,filename,requestProperty,callback).execute();
}
@Override
public void cancel() {
isCancel = true;
}
/**
* 异步下载任务
*/
private class DownloadTask extends AsyncTask<Void,Integer,File> {
private class DownloadTask extends AsyncTask<Void,Long,File> {
private String url;
private String path;
@ -78,12 +95,7 @@ public class HttpManager implements IHttpManager {
}
@Override
protected File doInBackground(Void... voids) {
try {
HttpsURLConnection.setDefaultSSLSocketFactory(SSLSocketFactoryUtils.createSSLSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(SSLSocketFactoryUtils.createTrustAllHostnameVerifier());
private File download(String url) throws Exception{
HttpURLConnection connect = (HttpURLConnection)new URL(url).openConnection();
connect.setRequestMethod("GET");
connect.setRequestProperty("Accept-Encoding", "identity");
@ -91,50 +103,84 @@ public class HttpManager implements IHttpManager {
connect.setReadTimeout(mTimeout);
connect.setConnectTimeout(mTimeout);
if(requestProperty!=null){
if(requestProperty != null){
for(Map.Entry<String,String> entry : requestProperty.entrySet()){
connect.setRequestProperty(entry.getKey(),entry.getValue());
}
}
connect.connect();
int responseCode = connect.getResponseCode();
Log.d(Constants.TAG,"Content-Type:" + connect.getContentType());
if(responseCode == HttpURLConnection.HTTP_OK){
int responseCode = connect.getResponseCode();
switch (responseCode){
case HttpURLConnection.HTTP_OK: {
InputStream is = connect.getInputStream();
InputStream is = connect.getInputStream();
long length = connect.getContentLength();
int length = connect.getContentLength();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
length = connect.getContentLengthLong();
}
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
length = (int)connect.getContentLengthLong();
}
Log.d(Constants.TAG, "contentLength:" + length);
int progress = 0;
long progress = 0;
byte[] buffer = new byte[4096];
byte[] buffer = new byte[8192];
int len;
File file = new File(path,filename);
FileOutputStream fos = new FileOutputStream(file);
while ((len = is.read(buffer)) != -1){
fos.write(buffer,0,len);
progress += len;
//更新进度
publishProgress(progress,length);
}
int len;
File file = new File(path, filename);
FileOutputStream fos = new FileOutputStream(file);
while ((len = is.read(buffer)) != -1) {
if (isCancel) {
cancel(true);
break;
}
fos.write(buffer, 0, len);
progress += len;
//更新进度
if (length > 0) {
publishProgress(progress, length);
}
}
fos.flush();
fos.close();
is.close();
connect.disconnect();
fos.flush();
fos.close();
is.close();
if(progress <= 0 && length <= 0){
throw new IllegalStateException(String.format("contentLength = %d",length));
}
connect.disconnect();
return file;
}
case HttpURLConnection.HTTP_MULT_CHOICE:
case HttpURLConnection.HTTP_MOVED_PERM:
case HttpURLConnection.HTTP_MOVED_TEMP:
case HttpURLConnection.HTTP_SEE_OTHER:
case HTTP_TEMP_REDIRECT:
case HTTP_PERM_REDIRECT: {//重定向
String redirectUrl = connect.getHeaderField("Location");
Log.d(Constants.TAG,"redirectUrl = " + redirectUrl);
connect.disconnect();
return download(redirectUrl);
}
default://连接失败
throw new ConnectException(String.format("responseCode = %d",responseCode));
return file;
}else {//连接失败
throw new ConnectException(String.format("responseCode = %d",responseCode));
}
}
@Override
protected File doInBackground(Void... voids) {
try{
HttpsURLConnection.setDefaultSSLSocketFactory(SSLSocketFactoryUtils.createSSLSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(SSLSocketFactoryUtils.createTrustAllHostnameVerifier());
return download(url);
} catch (Exception e) {
this.exception = e;
e.printStackTrace();
@ -146,7 +192,7 @@ public class HttpManager implements IHttpManager {
@Override
protected void onPreExecute() {
super.onPreExecute();
if(callback!=null){
if(callback != null){
callback.onStart(url);
}
}
@ -154,8 +200,8 @@ public class HttpManager implements IHttpManager {
@Override
protected void onPostExecute(File file) {
super.onPostExecute(file);
if(callback!=null){
if(file!=null){
if(callback != null){
if(file != null){
callback.onFinish(file);
}else{
callback.onError(exception);
@ -165,18 +211,20 @@ public class HttpManager implements IHttpManager {
}
@Override
protected void onProgressUpdate(Integer... values) {
protected void onProgressUpdate(Long... values) {
super.onProgressUpdate(values);
if(callback!=null){
callback.onProgress(values[0],values[1]);
if(callback != null){
if(!isCancelled()){
callback.onProgress(values[0],values[1]);
}
}
}
@Override
protected void onCancelled() {
super.onCancelled();
if(callback!=null){
if(callback != null){
callback.onCancel();
}
}

@ -7,6 +7,7 @@ import java.io.Serializable;
import java.util.Map;
/**
* IHttpManager 默认提供{@link HttpManager} {@link OkHttpManager}两种实现
* @author Jenly <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
public interface IHttpManager {
@ -22,6 +23,10 @@ public interface IHttpManager {
*/
void download(String url, String path, String filename, @Nullable Map<String,String> requestProperty, DownloadCallback callback);
/**
* 取消下载
*/
void cancel();
interface DownloadCallback extends Serializable{
/**
@ -35,7 +40,7 @@ public interface IHttpManager {
* @param progress
* @param total
*/
void onProgress(int progress,int total);
void onProgress(long progress,long total);
/**
* 完成

@ -0,0 +1,231 @@
package com.king.app.updater.http;
import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import com.king.app.updater.constant.Constants;
import com.king.app.updater.util.SSLSocketFactoryUtils;
import org.apache.http.conn.ssl.SSLSocketFactory;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.ConnectException;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
/**
* OkHttpManager使用{@link OkHttpClient}实现{@link IHttpManager}
* 使用OkHttpManager时必须依赖OkHttp库
* @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
public class OkHttpManager implements IHttpManager {
private static final int DEFAULT_TIME_OUT = 20000;
private OkHttpClient okHttpClient;
private boolean isCancel;
private static volatile OkHttpManager INSTANCE;
public static OkHttpManager getInstance(){
if(INSTANCE == null){
synchronized (HttpManager.class){
if(INSTANCE == null){
INSTANCE = new OkHttpManager();
}
}
}
return INSTANCE;
}
private OkHttpManager(){
this(DEFAULT_TIME_OUT);
}
/**
* HttpManager对外暴露如果没有特殊需求推荐使用{@link HttpManager#getInstance()}
* @param timeout 超时时间单位毫秒
*/
public OkHttpManager(int timeout){
this(new OkHttpClient.Builder()
.readTimeout(timeout, TimeUnit.MILLISECONDS)
.connectTimeout(timeout, TimeUnit.MILLISECONDS)
.sslSocketFactory(SSLSocketFactoryUtils.createSSLSocketFactory(),SSLSocketFactoryUtils.createTrustAllManager())
.hostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER)
.build());
}
/**
* HttpManager对外暴露推荐使用{@link HttpManager#getInstance()}
* @param okHttpClient {@link OkHttpClient}
*/
public OkHttpManager(@NonNull OkHttpClient okHttpClient){
this.okHttpClient = okHttpClient;
}
@Override
public void download(String url,final String path,final String filename, @Nullable Map<String, String> requestProperty,final DownloadCallback callback) {
isCancel = false;
new DownloadTask(okHttpClient,url,path,filename,requestProperty,callback).execute();
}
@Override
public void cancel() {
isCancel = true;
}
/**
* 异步下载任务
*/
private class DownloadTask extends AsyncTask<Void,Long,File> {
private String url;
private String path;
private String filename;
private Map<String,String> requestProperty;
private DownloadCallback callback;
private Exception exception;
private OkHttpClient okHttpClient;
public DownloadTask(OkHttpClient okHttpClient,String url, String path, String filename ,@Nullable Map<String,String> requestProperty, DownloadCallback callback){
this.okHttpClient = okHttpClient;
this.url = url;
this.path = path;
this.filename = filename;
this.callback = callback;
this.requestProperty = requestProperty;
}
@Override
protected File doInBackground(Void... voids) {
try{
Request.Builder builder = new Request.Builder()
.url(url)
.addHeader("Accept-Encoding", "identity")
.get();
if(requestProperty!=null){
for(Map.Entry<String,String> entry : requestProperty.entrySet()){
builder.addHeader(entry.getKey(),entry.getValue());
}
}
Call call = okHttpClient.newCall(builder.build());
Response response = call.execute();
if(response.isSuccessful()){
InputStream is = response.body().byteStream();
long length = response.body().contentLength();
Log.d(Constants.TAG,"contentLength:" + length);
long progress = 0;
byte[] buffer = new byte[8192];
int len;
File file = new File(path,filename);
FileOutputStream fos = new FileOutputStream(file);
while ((len = is.read(buffer)) != -1){
if(isCancel){
if(call != null){
call.cancel();
}
cancel(true);
break;
}
fos.write(buffer,0,len);
progress += len;
//更新进度
if(length > 0){
publishProgress(progress,length);
}
}
fos.flush();
fos.close();
is.close();
response.close();
if(progress <= 0 && length <= 0){
throw new IllegalStateException(String.format("contentLength = %d",length));
}
return file;
}else {//连接失败
throw new ConnectException(String.format("responseCode = %d",response.code()));
}
}catch (Exception e){
this.exception = e;
e.printStackTrace();
}
return null;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
if(callback != null){
callback.onStart(url);
}
}
@Override
protected void onPostExecute(File file) {
super.onPostExecute(file);
if(callback != null){
if(file != null){
callback.onFinish(file);
}else{
callback.onError(exception);
}
}
}
@Override
protected void onProgressUpdate(Long... values) {
super.onProgressUpdate(values);
if(callback != null){
if(!isCancelled()){
callback.onProgress(values[0],values[1]);
}
}
}
@Override
protected void onCancelled() {
super.onCancelled();
if(callback != null){
callback.onCancel();
}
}
}
}

@ -1,22 +1,13 @@
package com.king.app.updater.service;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.FileProvider;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
@ -27,6 +18,7 @@ import com.king.app.updater.constant.Constants;
import com.king.app.updater.http.HttpManager;
import com.king.app.updater.http.IHttpManager;
import com.king.app.updater.util.AppUtils;
import com.king.app.updater.util.NotificationUtils;
import java.io.File;
@ -54,6 +46,10 @@ public class DownloadService extends Service {
*/
private int mCount = 0;
private IHttpManager mHttpManager;
private File mFile;
private Context getContext(){
return this;
}
@ -62,20 +58,19 @@ public class DownloadService extends Service {
public int onStartCommand(Intent intent, int flags, int startId) {
if(intent != null){
if(!isDownloading){
boolean isStop = intent.getBooleanExtra(Constants.KEY_STOP_DOWNLOAD_SERVICE,false);
if(isStop){
stopService();
}else{
//是否实通过通知栏触发重复下载
boolean isReDownload = intent.getBooleanExtra(Constants.KEY_RE_DOWNLOAD,false);
if(isReDownload){
mCount++;
}
//获取配置信息
UpdateConfig config = intent.getParcelableExtra(Constants.KEY_UPDATE_CONFIG);
startDownload(config,null,null);
boolean isStop = intent.getBooleanExtra(Constants.KEY_STOP_DOWNLOAD_SERVICE,false);
if(isStop){
stopDownload();
} else if(!isDownloading){
//是否实通过通知栏触发重复下载
boolean isReDownload = intent.getBooleanExtra(Constants.KEY_RE_DOWNLOAD,false);
if(isReDownload){
mCount++;
}
//获取配置信息
UpdateConfig config = intent.getParcelableExtra(Constants.KEY_UPDATE_CONFIG);
startDownload(config,null,null);
}else{
Log.w(Constants.TAG,"Please do not repeat the download.");
}
@ -115,66 +110,87 @@ public class DownloadService extends Service {
//如果保存路径为空则使用缓存路径
if(TextUtils.isEmpty(path)){
path = getDiskCacheDir(getContext());
path = getExternalFilesDir(getContext());
}
File dirFile = new File(path);
if(!dirFile.exists()){
dirFile.mkdir();
dirFile.mkdirs();
}
//如果文件名为空则使用路径
if(TextUtils.isEmpty(filename)){
filename = AppUtils.INSTANCE.getAppFullName(getContext(),url,getResources().getString(R.string.app_name));
filename = AppUtils.getAppFullName(getContext(),url,getResources().getString(R.string.app_name));
}
File file = new File(path,filename);
if(file.exists()){//文件是否存在
mFile = new File(path,filename);
if(mFile.exists()){//文件是否存在
Integer versionCode = config.getVersionCode();
if(versionCode!=null){
String apkMD5 = config.getApkMD5();
//是否存在相同的apk
boolean isExistApk = false;
if(!TextUtils.isEmpty(apkMD5)){//如果存在MD5,则优先校验MD5
isExistApk = AppUtils.checkFileMD5(mFile,apkMD5);
}else if(versionCode!=null){//如果存在versionCode,则校验versionCode
try{
if(AppUtils.INSTANCE.apkExists(getContext(),versionCode,file)){
//本地已经存在要下载的APK
Log.d(Constants.TAG,"CacheFile:" + file);
if(config.isInstallApk()){
String authority = config.getAuthority();
if(TextUtils.isEmpty(authority)){//如果为空则默认
authority = getContext().getPackageName() + ".fileProvider";
}
AppUtils.INSTANCE.installApk(getContext(),file,authority);
}
if(callback!=null){
callback.onFinish(file);
}
stopService();
return;
}
isExistApk = AppUtils.apkExists(getContext(),versionCode,mFile);
}catch (Exception e){
Log.w(Constants.TAG,e);
}
}
if(isExistApk){
//本地已经存在要下载的APK
Log.d(Constants.TAG,"CacheFile:" + mFile);
if(config.isInstallApk()){
String authority = config.getAuthority();
if(TextUtils.isEmpty(authority)){//如果为空则默认
authority = getContext().getPackageName() + Constants.DEFAULT_FILE_PROVIDER;
}
AppUtils.installApk(getContext(),mFile,authority);
}
if(callback!=null){
callback.onFinish(mFile);
}
stopService();
return;
}
//删除旧文件
file.delete();
mFile.delete();
}
Log.d(Constants.TAG,"File:" + file);
Log.d(Constants.TAG,"File:" + mFile);
if(httpManager != null){
httpManager.download(url,path,filename,config.getRequestProperty(),new AppDownloadCallback(config,callback));
mHttpManager = httpManager;
}else{
HttpManager.getInstance().download(url,path,filename,config.getRequestProperty(),new AppDownloadCallback(config,callback));
mHttpManager = HttpManager.getInstance();
}
mHttpManager.download(url,path,filename,config.getRequestProperty(),new AppDownloadCallback(config,callback));
}
/**
* 停止下载
*/
public void stopDownload(){
if(mHttpManager!=null){
mHttpManager.cancel();
}
}
/**
* 获取缓存路径
* @param context
* @return
*/
public String getDiskCacheDir(Context context) {
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
return Constants.DEFAULT_DIR_PATH;
private String getExternalFilesDir(Context context) {
File[] files = ContextCompat.getExternalFilesDirs(context,Constants.DEFAULT_DIR);
if(files!=null && files.length > 0){
return files[0].getAbsolutePath();
}
return context.getExternalFilesDir(Constants.DEFAULT_DIR).getAbsolutePath();
return context.getCacheDir().getAbsolutePath();
}
/**
@ -210,21 +226,26 @@ public class DownloadService extends Service {
private boolean isReDownload;
private boolean isDeleteCancelFile;
private UpdateCallback callback;
private int reDownloads;
private AppDownloadCallback(UpdateConfig config,UpdateCallback callback){
this.config = config;
this.callback = callback;
this.isShowNotification = config.isShowNotification();
this.notifyId = config.getNotificationId();
this.reDownloads = config.getReDownloads();
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
this.channelId = TextUtils.isEmpty(config.getChannelId()) ? Constants.DEFAULT_NOTIFICATION_CHANNEL_ID : config.getChannelId();
this.channelName = TextUtils.isEmpty(config.getChannelName()) ? Constants.DEFAULT_NOTIFICATION_CHANNEL_NAME : config.getChannelName();
}
if(config.getNotificationIcon()<=0){
this.notificationIcon = AppUtils.INSTANCE.getAppIcon(getContext());
if(config.getNotificationIcon() <= 0){
this.notificationIcon = AppUtils.getAppIcon(getContext());
}else{
this.notificationIcon = config.getNotificationIcon();
}
@ -233,22 +254,22 @@ public class DownloadService extends Service {
this.authority = config.getAuthority();
if(TextUtils.isEmpty(config.getAuthority())){//如果为空则默认
authority = getContext().getPackageName() + ".fileProvider";
authority = getContext().getPackageName() + Constants.DEFAULT_FILE_PROVIDER;
}
this.isShowPercentage = config.isShowPercentage();
this.isReDownload = config.isReDownload();
this.isDeleteCancelFile = config.isDeleteCancelFile();
}
@Override
public void onStart(String url) {
Log.d(Constants.TAG,"onStart:" + url);
isDownloading = true;
mLastProgress = 0;
if(isShowNotification){
showStartNotification(notifyId,channelId,channelName,notificationIcon,getString(R.string.app_updater_start_notification_title),getString(R.string.app_updater_start_notification_content),config.isVibrate(),config.isSound());
NotificationUtils.showStartNotification(getContext(),notifyId,channelId,channelName,notificationIcon,getString(R.string.app_updater_start_notification_title),getString(R.string.app_updater_start_notification_content),config.isVibrate(),config.isSound());
}
if(callback!=null){
@ -257,7 +278,7 @@ public class DownloadService extends Service {
}
@Override
public void onProgress(int progress, int total) {
public void onProgress(long progress, long total) {
boolean isChange = false;
long curTime = System.currentTimeMillis();
@ -265,7 +286,7 @@ public class DownloadService extends Service {
mLastTime = curTime;
int currProgress = Math.round(progress * 1.0f / total * 100.0f);
if(currProgress!=mLastProgress){//百分比改变了才更新
if(currProgress != mLastProgress){//百分比改变了才更新
isChange = true;
String percentage = currProgress + "%";
if(isShowNotification) {
@ -275,7 +296,7 @@ public class DownloadService extends Service {
content += percentage;
}
showProgressNotification(notifyId, channelId, notificationIcon, getString(R.string.app_updater_progress_notification_title), content, progress, total);
NotificationUtils.showProgressNotification(getContext(),notifyId, channelId, notificationIcon, getString(R.string.app_updater_progress_notification_title), content, currProgress, 100);
}
}
@ -290,9 +311,9 @@ public class DownloadService extends Service {
public void onFinish(File file) {
Log.d(Constants.TAG,"onFinish:" + file);
isDownloading = false;
showFinishNotification(notifyId,channelId,notificationIcon,getString(R.string.app_updater_finish_notification_title),getString(R.string.app_updater_finish_notification_content),file,authority);
NotificationUtils.showFinishNotification(getContext(),notifyId,channelId,notificationIcon,getString(R.string.app_updater_finish_notification_title),getString(R.string.app_updater_finish_notification_content),file,authority);
if(isInstallApk){
AppUtils.INSTANCE.installApk(getContext(),file,authority);
AppUtils.installApk(getContext(),file,authority);
}
if(callback!=null){
callback.onFinish(file);
@ -302,12 +323,12 @@ public class DownloadService extends Service {
@Override
public void onError(Exception e) {
Log.w(Constants.TAG,e);
Log.w(Constants.TAG,"onError:"+ e.getMessage());
isDownloading = false;
//支持下载失败重新并最多支持失败下载3次
boolean isReDownload = this.isReDownload && mCount < 3;
//支持下载失败时重新下载,当重新下载次数不超过限制时才被允许
boolean isReDownload = this.isReDownload && mCount < reDownloads;
String content = isReDownload ? getString(R.string.app_updater_error_notification_content_re_download) : getString(R.string.app_updater_error_notification_content);
showErrorNotification(notifyId,channelId,notificationIcon,getString(R.string.app_updater_error_notification_title),content,isReDownload,config);
NotificationUtils.showErrorNotification(getContext(),notifyId,channelId,notificationIcon,getString(R.string.app_updater_error_notification_title),content,isReDownload,config);
if(callback!=null){
callback.onError(e);
@ -322,10 +343,13 @@ public class DownloadService extends Service {
public void onCancel() {
Log.d(Constants.TAG,"onCancel");
isDownloading = false;
cancelNotification(notifyId);
NotificationUtils.cancelNotification(getContext(),notifyId);
if(callback!=null){
callback.onCancel();
}
if(isDeleteCancelFile && mFile!=null){
mFile.delete();
}
stopService();
}
}
@ -333,202 +357,10 @@ public class DownloadService extends Service {
@Override
public void onDestroy() {
isDownloading = false;
mHttpManager = null;
super.onDestroy();
}
//---------------------------------------- Notification
/**
* 显示开始下载是的通知
* @param notifyId
* @param channelId
* @param channelName
* @param icon
* @param title
* @param content
*/
private void showStartNotification(int notifyId,String channelId, String channelName,@DrawableRes int icon,CharSequence title,CharSequence content,boolean isVibrate,boolean isSound){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
createNotificationChannel(channelId,channelName,isVibrate,isSound);
}
NotificationCompat.Builder builder = buildNotification(channelId,icon,title,content);
if(isVibrate && isSound){
builder.setDefaults(Notification.DEFAULT_VIBRATE | Notification.DEFAULT_SOUND);
}else if(isVibrate){
builder.setDefaults(Notification.DEFAULT_VIBRATE);
}else if(isSound){
builder.setDefaults(Notification.DEFAULT_SOUND);
}
Notification notification = builder.build();
notification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONLY_ALERT_ONCE;
notifyNotification(notifyId,notification);
}
/**
* 显示下载中的通知更新进度
* @param notifyId
* @param channelId
* @param icon
* @param title
* @param content
* @param progress
* @param size
*/
private void showProgressNotification(int notifyId,String channelId,@DrawableRes int icon,CharSequence title,CharSequence content,int progress,int size){
NotificationCompat.Builder builder = buildNotification(channelId,icon,title,content,progress,size);
Notification notification = builder.build();
notification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONLY_ALERT_ONCE;
notifyNotification(notifyId,notification);
}
/**
* 显示下载完成时的通知点击安装
* @param notifyId
* @param channelId
* @param icon
* @param title
* @param content
* @param file
*/
private void showFinishNotification(int notifyId,String channelId,@DrawableRes int icon,CharSequence title,CharSequence content,File file,String authority){
cancelNotification(notifyId);
NotificationCompat.Builder builder = buildNotification(channelId,icon,title,content);
builder.setAutoCancel(true);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addCategory(Intent.CATEGORY_DEFAULT);
Uri uriData;
String type = "application/vnd.android.package-archive";
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
uriData = FileProvider.getUriForFile(getContext(), authority, file);
}else{
uriData = Uri.fromFile(file);
}
intent.setDataAndType(uriData, type);
PendingIntent clickIntent = PendingIntent.getActivity(getContext(), notifyId,intent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(clickIntent);
Notification notification = builder.build();
notification.flags = Notification.FLAG_AUTO_CANCEL;
notifyNotification(notifyId,notification);
}
private void showErrorNotification(int notifyId,String channelId,@DrawableRes int icon,CharSequence title,CharSequence content,boolean isReDownload,UpdateConfig config){
NotificationCompat.Builder builder = buildNotification(channelId,icon,title,content);
builder.setAutoCancel(true);
if(isReDownload){//重新下载
Intent intent = new Intent(getContext(),DownloadService.class);
intent.putExtra(Constants.KEY_RE_DOWNLOAD,true);
intent.putExtra(Constants.KEY_UPDATE_CONFIG,config);
PendingIntent clickIntent = PendingIntent.getService(getContext(), notifyId,intent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(clickIntent);
}else{
PendingIntent clickIntent = PendingIntent.getService(getContext(), notifyId,new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(clickIntent);
}
Notification notification = builder.build();
notification.flags = Notification.FLAG_AUTO_CANCEL;
notifyNotification(notifyId,notification);
}
/**
* 显示通知信息非第一次
* @param notifyId
* @param channelId
* @param icon
* @param title
* @param content
*/
private void showNotification(int notifyId,String channelId,@DrawableRes int icon,CharSequence title,CharSequence content,boolean isAutoCancel){
NotificationCompat.Builder builder = buildNotification(channelId,icon,title,content);
builder.setAutoCancel(isAutoCancel);
Notification notification = builder.build();
notification.flags = Notification.FLAG_AUTO_CANCEL;
notifyNotification(notifyId,notification);
}
/**
*
* @param notifyId
*/
private void cancelNotification(int notifyId){
getNotificationManager().cancel(notifyId);
}
/**
* 获取通知管理器
* @return
*/
private NotificationManager getNotificationManager(){
return (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
}
/**
* 创建一个通知渠道兼容0以上版本
* @param channelId
* @param channelName
*/
@RequiresApi(api = Build.VERSION_CODES.O)
private void createNotificationChannel(String channelId, String channelName,boolean isVibrate,boolean isSound){
NotificationChannel channel = new NotificationChannel(channelId,channelName, NotificationManager.IMPORTANCE_DEFAULT);
channel.enableVibration(isVibrate);
if(!isSound){
channel.setSound(null,null);
}
getNotificationManager().createNotificationChannel(channel);
}
/**
* 构建一个通知构建器
* @param channelId
* @param icon
* @param title
* @param content
* @return
*/
private NotificationCompat.Builder buildNotification(String channelId, @DrawableRes int icon,CharSequence title,CharSequence content){
return buildNotification(channelId,icon,title,content,Constants.NONE,Constants.NONE);
}
/**
* 构建一个通知构建器
* @param channelId
* @param icon
* @param title
* @param content
* @param progress
* @param size
* @return
*/
private NotificationCompat.Builder buildNotification(String channelId,@DrawableRes int icon,CharSequence title,CharSequence content,int progress,int size){
NotificationCompat.Builder builder = new NotificationCompat.Builder(getContext(),channelId);
builder.setSmallIcon(icon);
builder.setContentTitle(title);
builder.setContentText(content);
builder.setOngoing(true);
if(progress!= Constants.NONE && size!=Constants.NONE){
builder.setProgress(size,progress,false);
}
return builder;
}
/**
* 更新通知栏
* @param id
* @param notification
*/
private void notifyNotification(int id, Notification notification){
getNotificationManager().notify(id,notification);
}
//---------------------------------------- Binder
@Nullable
@ -557,4 +389,4 @@ public class DownloadService extends Service {
}
}
}

@ -1,30 +1,42 @@
package com.king.app.updater.util;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.net.Uri;
import android.os.Build;
import android.support.v4.content.FileProvider;
import android.text.TextUtils;
import android.util.Log;
import com.king.app.updater.constant.Constants;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
/**
* @author Jenly <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
public enum AppUtils {
public final class AppUtils {
INSTANCE;
private AppUtils(){
throw new AssertionError();
}
/**
* 通过url获取App的全名称
* @param context
* @return AppName.apk
*/
public String getAppFullName(Context context,String url,String defaultName){
public static String getAppFullName(Context context,String url,String defaultName){
if(url.endsWith(".apk")){
return url.substring(url.lastIndexOf("/") + 1);
}
@ -44,7 +56,7 @@ public enum AppUtils {
* @return
* @throws PackageManager.NameNotFoundException
*/
public PackageInfo getPackageInfo(Context context) throws PackageManager.NameNotFoundException {
public static PackageInfo getPackageInfo(Context context) throws PackageManager.NameNotFoundException {
PackageManager packageManager = context.getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo( context.getPackageName(), 0);
return packageInfo;
@ -66,7 +78,7 @@ public enum AppUtils {
/**
* 获取App的名称
*/
public String getAppName(Context context) {
public static String getAppName(Context context) {
try{
int labelRes = getPackageInfo(context).applicationInfo.labelRes;
@ -82,7 +94,7 @@ public enum AppUtils {
* @param context
* @return
*/
public int getAppIcon(Context context){
public static int getAppIcon(Context context){
try{
return getPackageInfo(context).applicationInfo.icon;
} catch (Exception e) {
@ -97,12 +109,24 @@ public enum AppUtils {
* @param context
* @param file
*/
public void installApk(Context context,File file,String authority){
public static void installApk(Context context,File file,String authority){
Intent intent = getInstallIntent(context,file,authority);
context.startActivity(intent);
}
/**
* 获取安装Intent
* @param context
* @param file
* @param authority
* @return
*/
public static Intent getInstallIntent(Context context,File file,String authority){
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addCategory(Intent.CATEGORY_DEFAULT);
Uri uriData = null;
Uri uriData;
String type = "application/vnd.android.package-archive";
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N){
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
@ -111,7 +135,7 @@ public enum AppUtils {
uriData = Uri.fromFile(file);
}
intent.setDataAndType(uriData, type);
context.startActivity(intent);
return intent;
}
/**
@ -122,7 +146,7 @@ public enum AppUtils {
* @return
* @throws Exception
*/
public boolean apkExists(Context context,int versionCode,File file) throws Exception{
public static boolean apkExists(Context context,int versionCode,File file) throws Exception{
if(file!=null && file.exists()){
String packageName = context.getPackageName();
PackageInfo packageInfo = AppUtils.getPackageInfo(context,file.getAbsolutePath());
@ -135,4 +159,94 @@ public enum AppUtils {
}
return false;
}
/**
* 判断文件是否存在
* @param context
* @param path
* @return
*/
public static boolean isAndroidQFileExists(Context context,String path){
return isAndroidQFileExists(context,new File(path));
}
/**
* 判断文件是否存在
* @param context
* @param file
* @return
*/
public static boolean isAndroidQFileExists(Context context,File file){
AssetFileDescriptor descriptor = null;
ContentResolver contentResolver = context.getContentResolver();
try {
Uri uri = Uri.fromFile(file);
descriptor = contentResolver.openAssetFileDescriptor(uri, "r");
if (descriptor == null) {
return false;
} else {
close(descriptor);
}
return true;
} catch (FileNotFoundException e) {
}finally {
close(descriptor);
}
return false;
}
/**
* 校验文件MD5
* @param file
* @param md5
* @return
*/
public static boolean checkFileMD5(File file,String md5){
String fileMD5 = getFileMD5(file);
Log.d(Constants.TAG,"FileMD5:"+ fileMD5);
if(!TextUtils.isEmpty(md5)){
return md5.equalsIgnoreCase(fileMD5);
}
return false;
}
/**
* 获取文件MD5
* @param file
* @return
*/
public static String getFileMD5(File file){
try {
FileInputStream fis = new FileInputStream(file);
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[1024];
int length;
while ((length = fis.read(buffer)) != -1){
messageDigest.update(buffer,0,length);
}
BigInteger bigInteger = new BigInteger(1,messageDigest.digest());
return bigInteger.toString(16);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 关闭
* @param descriptor
*/
private static void close(AssetFileDescriptor descriptor){
if(descriptor != null){
try {
descriptor.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

@ -0,0 +1,222 @@
package com.king.app.updater.util;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.support.annotation.DrawableRes;
import android.support.annotation.RequiresApi;
import android.support.v4.app.NotificationCompat;
import com.king.app.updater.UpdateConfig;
import com.king.app.updater.constant.Constants;
import com.king.app.updater.service.DownloadService;
import java.io.File;
/**
* @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
public class NotificationUtils {
private NotificationUtils(){
throw new AssertionError();
}
/**
* 显示开始下载是的通知
* @param notifyId
* @param channelId
* @param channelName
* @param icon
* @param title
* @param content
*/
public static void showStartNotification(Context context, int notifyId,String channelId, String channelName,@DrawableRes int icon,CharSequence title,CharSequence content,boolean isVibrate,boolean isSound){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
createNotificationChannel(context,channelId,channelName,isVibrate,isSound);
}
NotificationCompat.Builder builder = buildNotification(context,channelId,icon,title,content);
builder.setPriority(NotificationManager.IMPORTANCE_HIGH);
if(isVibrate && isSound){
builder.setDefaults(Notification.DEFAULT_VIBRATE | Notification.DEFAULT_SOUND);
}else if(isVibrate){
builder.setDefaults(Notification.DEFAULT_VIBRATE);
}else if(isSound){
builder.setDefaults(Notification.DEFAULT_SOUND);
}
Notification notification = builder.build();
notification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONLY_ALERT_ONCE;
notifyNotification(context,notifyId,notification);
}
/**
* 显示下载中的通知更新进度
* @param notifyId
* @param channelId
* @param icon
* @param title
* @param content
* @param progress
* @param size
*/
public static void showProgressNotification(Context context, int notifyId,String channelId,@DrawableRes int icon,CharSequence title,CharSequence content,int progress,int size){
NotificationCompat.Builder builder = buildNotification(context,channelId,icon,title,content,progress,size);
Notification notification = builder.build();
notification.flags = Notification.FLAG_NO_CLEAR | Notification.FLAG_ONLY_ALERT_ONCE;
notifyNotification(context,notifyId,notification);
}
/**
* 显示下载完成时的通知点击安装
* @param notifyId
* @param channelId
* @param icon
* @param title
* @param content
* @param file
*/
public static void showFinishNotification(Context context, int notifyId, String channelId, @DrawableRes int icon, CharSequence title, CharSequence content, File file, String authority){
cancelNotification(context,notifyId);
NotificationCompat.Builder builder = buildNotification(context,channelId,icon,title,content);
builder.setAutoCancel(true);
Intent intent = AppUtils.getInstallIntent(context,file,authority);
PendingIntent clickIntent = PendingIntent.getActivity(context, notifyId,intent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(clickIntent);
Notification notification = builder.build();
notification.flags = Notification.FLAG_AUTO_CANCEL;
notifyNotification(context,notifyId,notification);
}
/**
* 现在下载失败通知
* @param context
* @param notifyId
* @param channelId
* @param icon
* @param title
* @param content
* @param isReDownload
* @param config
*/
public static void showErrorNotification(Context context, int notifyId, String channelId, @DrawableRes int icon, CharSequence title, CharSequence content, boolean isReDownload, UpdateConfig config){
NotificationCompat.Builder builder = buildNotification(context,channelId,icon,title,content);
builder.setAutoCancel(true);
if(isReDownload){//重新下载
Intent intent = new Intent(context, DownloadService.class);
intent.putExtra(Constants.KEY_RE_DOWNLOAD,true);
intent.putExtra(Constants.KEY_UPDATE_CONFIG,config);
PendingIntent clickIntent = PendingIntent.getService(context, notifyId,intent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(clickIntent);
}else{
PendingIntent clickIntent = PendingIntent.getService(context, notifyId,new Intent(), PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(clickIntent);
}
Notification notification = builder.build();
notification.flags = Notification.FLAG_AUTO_CANCEL;
notifyNotification(context,notifyId,notification);
}
/**
* 显示通知信息非第一次
* @param notifyId
* @param channelId
* @param icon
* @param title
* @param content
*/
public static void showNotification(Context context, int notifyId,String channelId,@DrawableRes int icon,CharSequence title,CharSequence content,boolean isAutoCancel){
NotificationCompat.Builder builder = buildNotification(context,channelId,icon,title,content);
builder.setAutoCancel(isAutoCancel);
Notification notification = builder.build();
notification.flags = Notification.FLAG_AUTO_CANCEL;
notifyNotification(context,notifyId,notification);
}
/**
* 取消通知
* @param notifyId
*/
public static void cancelNotification(Context context, int notifyId){
getNotificationManager(context).cancel(notifyId);
}
/**
* 获取通知管理器
* @return
*/
public static NotificationManager getNotificationManager(Context context){
return (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
}
/**
* 创建一个通知渠道兼容0以上版本
* @param channelId
* @param channelName
*/
@RequiresApi(api = Build.VERSION_CODES.O)
public static void createNotificationChannel(Context context, String channelId, String channelName,boolean isVibrate,boolean isSound){
NotificationChannel channel = new NotificationChannel(channelId,channelName, NotificationManager.IMPORTANCE_HIGH);
channel.enableVibration(isVibrate);
if(!isSound){
channel.setSound(null,null);
}
getNotificationManager(context).createNotificationChannel(channel);
}
/**
* 构建一个通知构建器
* @param channelId
* @param icon
* @param title
* @param content
* @return
*/
private static NotificationCompat.Builder buildNotification(Context context, String channelId, @DrawableRes int icon,CharSequence title,CharSequence content){
return buildNotification(context,channelId,icon,title,content,Constants.NONE,Constants.NONE);
}
/**
* 构建一个通知构建器
* @param channelId
* @param icon
* @param title
* @param content
* @param progress
* @param size
* @return
*/
private static NotificationCompat.Builder buildNotification(Context context, String channelId, @DrawableRes int icon, CharSequence title, CharSequence content, int progress, int size){
NotificationCompat.Builder builder = new NotificationCompat.Builder(context,channelId);
builder.setSmallIcon(icon);
builder.setContentTitle(title);
builder.setContentText(content);
builder.setOngoing(true);
if(progress!= Constants.NONE && size!=Constants.NONE){
builder.setProgress(size,progress,false);
}
return builder;
}
/**
* 更新通知栏
* @param id
* @param notification
*/
private static void notifyNotification(Context context, int id, Notification notification){
getNotificationManager(context).notify(id,notification);
}
}

@ -1,7 +1,6 @@
package com.king.app.updater.util;
import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AppOpsManager;
import android.app.NotificationManager;
@ -18,22 +17,24 @@ import java.lang.reflect.Method;
/**
* @author Jenly <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
public enum PermissionUtils {
public final class PermissionUtils {
INSTANCE;
private String[] PERMISSIONS_STORAGE = {
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_PHONE_STATE};
private PermissionUtils(){
throw new AssertionError();
}
/**
* 校验权限
* @param activity
* @param requestCode
* @return
*/
public boolean verifyReadAndWritePermissions(@NonNull Activity activity,int requestCode){
public static boolean verifyReadAndWritePermissions(@NonNull Activity activity,int requestCode){
int readResult = checkPermission(activity,Manifest.permission.READ_EXTERNAL_STORAGE);
int writeResult = checkPermission(activity,Manifest.permission.WRITE_EXTERNAL_STORAGE);
@ -45,7 +46,7 @@ public enum PermissionUtils {
return true;
}
public int checkPermission(@NonNull Activity activity,@NonNull String permission){
public static int checkPermission(@NonNull Activity activity,@NonNull String permission){
return ActivityCompat.checkSelfPermission(activity,permission);
}
@ -53,7 +54,7 @@ public enum PermissionUtils {
* 获取通知权限
* @param context
*/
public boolean isNotificationEnabled(Context context) {
public static boolean isNotificationEnabled(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);

@ -24,10 +24,10 @@ import javax.net.ssl.X509TrustManager;
/**
* @author Jenly <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
public class SSLSocketFactoryUtils {
public final class SSLSocketFactoryUtils {
private SSLSocketFactoryUtils(){
throw new AssertionError();
}
public static SSLSocketFactory createSSLSocketFactory() {
@ -107,7 +107,7 @@ public class SSLSocketFactoryUtils {
return null;
}
//获得服务器端证书
TrustManager[] turstManager = getTurstManager(certificates);
TrustManager[] turstManager = getTrustManager(certificates);
//初始化ssl证书库
try {
@ -129,7 +129,7 @@ public class SSLSocketFactoryUtils {
* @param certificates
* @return
*/
public static TrustManager[] getTurstManager(InputStream... certificates) {
public static TrustManager[] getTrustManager(InputStream... certificates) {
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
@ -161,13 +161,13 @@ public class SSLSocketFactoryUtils {
}
return getTurstAllManager();
return getTrustAllManager();
}
/**
* 获得信任所有服务器端证书库
* */
public static TrustManager[] getTurstAllManager() {
public static TrustManager[] getTrustAllManager() {
return new X509TrustManager[] {createTrustAllManager()};
}

@ -5,7 +5,7 @@
<string name="app_updater_start_notification_content">正在获取下载数据…</string>
<string name="app_updater_progress_notification_title">版本更新</string>
<string name="app_updater_progress_notification_content">下载更新中</string>
<string name="app_updater_progress_notification_content">正在下载…</string>
<string name="app_updater_finish_notification_title">下载完成</string>
<string name="app_updater_finish_notification_content">点击安装</string>

@ -2,10 +2,8 @@
<paths>
<root-path name="app_root_path" path="/"/>
<external-path name="app_external_path" path="/"/>
<external-path name="app_updater_path" path=".AppUpdater/"/>
<external-cache-path name="app_external_cache_path" path="/"/>
<external-files-path name="app_external_files_path" path="/"/>
<files-path name="app_files_path" path="/"/>
<cache-path name="app_cache_path" path="/"/>
</paths>

@ -30,6 +30,7 @@ dependencies {
//support
implementation deps.support.appcompat
implementation deps.support.constraintlayout
implementation deps.okhttp
implementation project(':app-updater')
implementation project(':app-dialog')

Binary file not shown.

@ -1 +1 @@
[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":5,"versionName":"1.0.4","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]
[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":17,"versionName":"1.0.10","enabled":true,"outputFile":"app-release.apk","fullName":"release","baseName":"release"},"path":"app-release.apk","properties":{}}]

@ -3,7 +3,6 @@ package com.king.appupdater;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
@ -20,28 +19,37 @@ import com.king.app.updater.UpdateConfig;
import com.king.app.updater.callback.AppUpdateCallback;
import com.king.app.updater.callback.UpdateCallback;
import com.king.app.updater.constant.Constants;
import com.king.app.updater.http.OkHttpManager;
import com.king.app.updater.util.PermissionUtils;
import java.io.File;
/**
* @author <a href="mailto:jenly1314@gmail.com">Jenly</a>
*/
public class MainActivity extends AppCompatActivity {
private final Object mLock = new Object();
private String mUrl = "https://raw.githubusercontent.com/jenly1314/AppUpdater/master/app/release/app-release.apk";
//下载出现Failed to connect to raw.githubusercontent.com时,可以换个下载链接测试,github的raw.githubusercontent.com目前不太稳定。
// private String mUrl = "https://raw.githubusercontent.com/jenly1314/AppUpdater/master/app/release/app-release.apk";
private String mUrl = "https://gitlab.com/jenly1314/AppUpdater/-/raw/master/app/release/app-release.apk";
private ProgressBar progressBar;
private Toast toast;
private AppUpdater mAppUpdater;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressBar = findViewById(R.id.progressBar);
progressBar.setVisibility(View.INVISIBLE);
progressBar.setMax(100);
PermissionUtils.INSTANCE.verifyReadAndWritePermissions(this,Constants.RE_CODE_STORAGE_PERMISSION);
PermissionUtils.verifyReadAndWritePermissions(this,Constants.RE_CODE_STORAGE_PERMISSION);
}
public Context getContext(){
@ -64,7 +72,8 @@ public class MainActivity extends AppCompatActivity {
* 简单一键后台升级
*/
private void clickBtn1(){
new AppUpdater(getContext(),mUrl).start();
mAppUpdater = new AppUpdater(getContext(),mUrl);
mAppUpdater.start();
}
/**
@ -74,7 +83,8 @@ public class MainActivity extends AppCompatActivity {
UpdateConfig config = new UpdateConfig();
config.setUrl(mUrl);
config.addHeader("token","xxxxxx");
new AppUpdater(getContext(),config)
mAppUpdater = new AppUpdater(getContext(),config)
.setHttpManager(OkHttpManager.getInstance())
.setUpdateCallback(new UpdateCallback() {
@Override
@ -91,10 +101,10 @@ public class MainActivity extends AppCompatActivity {
}
@Override
public void onProgress(int progress, int total, boolean isChange) {
public void onProgress(long progress, long total, boolean isChange) {
if(isChange){
progressBar.setMax(total);
progressBar.setProgress(progress);
int currProgress = Math.round(progress * 1.0f / total * 100.0f);
progressBar.setProgress(currProgress);
}
}
@ -112,8 +122,8 @@ public class MainActivity extends AppCompatActivity {
public void onCancel() {
progressBar.setVisibility(View.INVISIBLE);
}
})
.start();
});
mAppUpdater.start();
}
/**
@ -126,12 +136,12 @@ public class MainActivity extends AppCompatActivity {
.setPositiveButton("升级", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new AppUpdater.Builder()
.serUrl(mUrl)
mAppUpdater = new AppUpdater.Builder()
.setUrl(mUrl)
.build(getContext())
.setUpdateCallback(new AppUpdateCallback() {
@Override
public void onProgress(int progress, int total, boolean isChange) {
public void onProgress(long progress, long total, boolean isChange) {
}
@ -139,8 +149,8 @@ public class MainActivity extends AppCompatActivity {
public void onFinish(File file) {
showToast("下载完成");
}
})
.start();
});
mAppUpdater.start();
}
}).show();
}
@ -149,14 +159,15 @@ public class MainActivity extends AppCompatActivity {
* 简单弹框升级
*/
private void clickBtn4(){
AppDialogConfig config = new AppDialogConfig();
AppDialogConfig config = new AppDialogConfig(getContext());
config.setTitle("简单弹框升级")
.setOk("升级")
.setContent("1、新增某某功能、\n2、修改某某问题、\n3、优化某某BUG、")
.setOnClickOk(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AppUpdater(getContext(),mUrl).start();
mAppUpdater = new AppUpdater(getContext(),mUrl);
mAppUpdater.start();
AppDialog.INSTANCE.dismissDialog();
}
});
@ -167,23 +178,23 @@ public class MainActivity extends AppCompatActivity {
* 简单自定义弹框升级
*/
private void clickBtn5(){
AppDialogConfig config = new AppDialogConfig();
config.setLayoutId(R.layout.dialog)
.setOk("升级")
AppDialogConfig config = new AppDialogConfig(getContext(),R.layout.dialog);
config.setOk("升级")
.setHideCancel(true)
.setTitle("简单自定义弹框升级")
.setContent("1、新增某某功能、\n2、修改某某问题、\n3、优化某某BUG、")
.setOnClickOk(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AppUpdater.Builder()
.serUrl(mUrl)
.build(getContext())
.start();
mAppUpdater = new AppUpdater.Builder()
.setUrl(mUrl)
.build(getContext());
mAppUpdater.start();
AppDialog.INSTANCE.dismissDialog();
}
});
AppDialog.INSTANCE.showDialog(getContext(),AppDialog.INSTANCE.createAppDialogView(getContext(),config),true);
//强制升级,拦截返回
AppDialog.INSTANCE.showDialog(getContext(),AppDialog.INSTANCE.createAppDialogView(getContext(),config),false);
}
/**
@ -208,14 +219,16 @@ public class MainActivity extends AppCompatActivity {
btnOK.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AppUpdater.Builder()
.serUrl(mUrl)
.setPath(Environment.getExternalStorageDirectory() + "/.AppUpdater")
.setVersionCode(BuildConfig.VERSION_CODE)//设置versionCode之后,新版本相同的apk只下载一次,优先取本地缓存。
.setFilename("AppUpdater1.apk")
mAppUpdater = new AppUpdater.Builder()
.setUrl(mUrl)
// .setPath(Environment.getExternalStorageDirectory() + "/.AppUpdater")//如果适配Android Q,则Environment.getExternalStorageDirectory()将废弃
// .setPath(getExternalFilesDir(Constants.DEFAULT_DIR).getAbsolutePath())//自定义路径,推荐使用默认
// .setApkMD5("3df5b1c1d2bbd01b4a7ddb3f2722ccca")//支持MD5校验,如果缓存APK的MD5与此MD5相同,则直接取本地缓存安装,推荐使用MD5校验的方式
.setVersionCode(BuildConfig.VERSION_CODE)//支持versionCode校验,设置versionCode之后,新版本versionCode相同的apk只下载一次,优先取本地缓存,推荐使用MD5校验的方式
.setFilename("AppUpdater.apk")
.setVibrate(true)
.build(getContext())
.start();
.build(getContext());
mAppUpdater.setHttpManager(OkHttpManager.getInstance()).start();
AppDialog.INSTANCE.dismissDialog();
}
});
@ -227,19 +240,19 @@ public class MainActivity extends AppCompatActivity {
* 简单DialogFragment升级
*/
private void clickBtn7(){
AppDialogConfig config = new AppDialogConfig();
AppDialogConfig config = new AppDialogConfig(getContext());
config.setTitle("简单DialogFragment升级")
.setOk("升级")
.setContent("1、新增某某功能、\n2、修改某某问题、\n3、优化某某BUG、")
.setOnClickOk(new View.OnClickListener() {
@Override
public void onClick(View v) {
new AppUpdater.Builder()
.serUrl(mUrl)
mAppUpdater = new AppUpdater.Builder()
.setUrl(mUrl)
.setVibrate(true)
.setSound(true)
.build(getContext())
.start();
.build(getContext());
mAppUpdater.setHttpManager(OkHttpManager.getInstance()).start();
AppDialog.INSTANCE.dismissDialogFragment(getSupportFragmentManager());
}
});
@ -247,7 +260,14 @@ public class MainActivity extends AppCompatActivity {
}
public void OnClick(View v){
private void clickCancel(){
if(mAppUpdater != null){
mAppUpdater.stop();
}
}
public void onClick(View v){
switch (v.getId()){
case R.id.btn1:
clickBtn1();
@ -270,6 +290,9 @@ public class MainActivity extends AppCompatActivity {
case R.id.btn7:
clickBtn7();
break;
case R.id.btnCancel:
clickCancel();
break;
}
}
}
}

@ -104,4 +104,17 @@
app:layout_constraintRight_toRightOf="parent"
style="@style/OnClick"/>
<Button
android:id="@+id/btnCancel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="10dp"
android:text="取消下载"
app:layout_constraintTop_toBottomOf="@+id/btn7"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
style="@style/OnClick"/>
</android.support.constraint.ConstraintLayout>

@ -11,8 +11,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:minHeight="40dp"
android:padding="6dp"
android:padding="10dp"
android:lines="1"
android:textSize="16sp"
android:background="@drawable/dialog_title_bg"
@ -22,8 +21,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:layout_marginBottom="6dp"
android:lineSpacingMultiplier="1" />
android:textSize="@dimen/app_dialog_content_text_size"
android:textColor="@color/app_dialog_content_color"
android:layout_marginBottom="16dp"
android:lineSpacingMultiplier="1.2" />
<include layout="@layout/app_dialog_line_h"/>
<LinearLayout
android:orientation="horizontal"

@ -11,7 +11,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:minHeight="40dp"
android:padding="6dp"
android:lines="1"
android:textSize="16sp"

@ -11,6 +11,6 @@
<style name="OnClick">
<item name="android:clickable">true</item>
<item name="android:onClick">OnClick</item>
<item name="android:onClick">onClick</item>
</style>
</resources>

@ -6,12 +6,16 @@ buildscript {
addRepos(repositories)
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath 'com.android.tools.build:gradle:4.1.1'
classpath 'com.novoda:bintray-release:0.9'
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.13.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {

@ -11,3 +11,23 @@ org.gradle.jvmargs=-Xmx1536m
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
VERSION_NAME=1.0.10
VERSION_CODE=17
GROUP=com.github.jenly1314.AppUpdater
POM_DESCRIPTION=AppUpdater for Android
POM_URL=https://github.com/jenly1314/AppUpdater
POM_SCM_URL=https://github.com/jenly1314/AppUpdater
POM_SCM_CONNECTION=scm:git@github.com:jenly1314/AppUpdater.git
POM_SCM_DEV_CONNECTION=scm:git@github.com:jenly1314/AppUpdater.git
POM_LICENCE_NAME=The MIT License
POM_LICENCE_URL=https://opensource.org/licenses/mit-license.php
#POM_LICENCE_NAME=The Apache Software License, Version 2.0
#POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
POM_DEVELOPER_ID=jenly
POM_DEVELOPER_NAME=Jenly Yu
RELEASE_REPOSITORY_URL=https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/
SNAPSHOT_REPOSITORY_URL=https://s01.oss.sonatype.org/content/repositories/snapshots/
RELEASE_SIGNING_ENABLED=true

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip

@ -1,7 +1,7 @@
//App
def app_version = [:]
app_version.versionCode = 5 //androidx 6
app_version.versionName = "1.0.4"
app_version.versionCode = 17 //androidx 18
app_version.versionName = "1.0.10"
ext.app_version = app_version
//build version
@ -22,6 +22,9 @@ versions.constraintLayout = "1.1.3"
versions.junit = "4.12"
versions.runner = "1.0.2"
versions.espresso = "3.0.2"
versions.okhttp = "4.2.2"
ext.versions = versions
ext.deps = [:]
@ -39,11 +42,15 @@ test.runner = "com.android.support.test:runner:$versions.runner"
test.espresso = "com.android.support.test.espresso:espresso-core:$versions.espresso"
deps.test = test
//okHttp
deps.okhttp = "com.squareup.okhttp3:okhttp:$versions.okhttp"
ext.deps = deps
def addRepos(RepositoryHandler handler) {
handler.google()
handler.mavenCentral()
handler.jcenter()
}
ext.addRepos = this.&addRepos
Loading…
Cancel
Save