mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	merge double upstream
This commit is contained in:
		
							
								
								
									
										24
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| * text=auto | ||||
| * text eol=lf | ||||
|  | ||||
| # Windows forced line-endings | ||||
| /.idea/* text eol=crlf | ||||
|  | ||||
| # Gradle wrapper | ||||
| *.jar binary | ||||
|  | ||||
| # Images | ||||
| *.webp binary | ||||
| *.png binary | ||||
| *.jpg binary | ||||
| *.jpeg binary | ||||
| *.gif binary | ||||
| *.ico binary | ||||
| *.gz binary | ||||
| *.zip binary | ||||
| *.7z binary | ||||
| *.ttf binary | ||||
| *.eot binary | ||||
| *.woff binary | ||||
| *.pyc binary | ||||
| *.swp binary | ||||
							
								
								
									
										6
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/CONTRIBUTING.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,10 +1,10 @@ | ||||
| 1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).** | ||||
| 1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).** | ||||
| 2. If you are unsure, ask here: [](https://discord.gg/tachiyomi) | ||||
| 3. What is your type of issue? | ||||
|     * [Catalogue request](#catalogue-requests) | ||||
|     * [Bugs](#bugs) | ||||
|     * [Feature requests](#feature-requests) | ||||
|     * [Translations](https://github.com/inorichi/tachiyomi/wiki/Translation) | ||||
|     * [Translations](https://tachiyomi.org/help/contribution/#translation) | ||||
| 4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new) | ||||
|  | ||||
| *** | ||||
| @@ -29,5 +29,5 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75 | ||||
|  | ||||
| # Feature requests | ||||
|  | ||||
| * Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does" | ||||
| * Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does" | ||||
| * Include screenshot (if needed) | ||||
|   | ||||
							
								
								
									
										29
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										29
									
								
								.github/ISSUE_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,17 +1,26 @@ | ||||
| **DO NOT OPEN ISSUES/REQUESTS RELATING TO EXTENSIONS/CATALOGUES IN THIS REPOSITORY. Open them at the following repository  https://github.com/inorichi/tachiyomi-extensions/** | ||||
| **PLEASE READ THIS** | ||||
|  | ||||
| **For all other requests Please fill out the form below and remove the first 3 lines of this template** | ||||
| I acknowledge that: | ||||
|  | ||||
| **App version:** | ||||
| - I have updated to the latest version of the app (stable is v0.9.2) | ||||
| - I have updated all extensions | ||||
| - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions | ||||
|  | ||||
| **Android version:** | ||||
| **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT** | ||||
|  | ||||
| **Issue/Request:** | ||||
| --- | ||||
|  | ||||
| **Steps to reproduce (if applicable)** | ||||
| ### Device information | ||||
| * Tachiyomi version: ? | ||||
| * Android version: ? | ||||
| * Device: ? | ||||
|  | ||||
|  1. | ||||
|  2. | ||||
|  3. | ||||
| ## Steps to reproduce | ||||
| 1. First step | ||||
| 2. Second step | ||||
|  | ||||
| **Other details:** | ||||
| ## Issue/Request | ||||
| ? | ||||
|  | ||||
| ## Other details | ||||
| Additional details and attachments. | ||||
|   | ||||
							
								
								
									
										12
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/ISSUE_TEMPLATE/bug_report.md
									
									
									
									
										vendored
									
									
								
							| @@ -3,12 +3,24 @@ name: "🐞 Bug report" | ||||
| about: Report a bug | ||||
| title: "[Bug] Write short description here" | ||||
| labels: "bug" | ||||
| --- | ||||
|  | ||||
| **PLEASE READ THIS** | ||||
|  | ||||
| I acknowledge that: | ||||
|  | ||||
| - I have updated to the latest version of the app (stable is v0.9.2) | ||||
| - I have updated all extensions | ||||
| - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions | ||||
|  | ||||
| **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT** | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### Device information | ||||
| * Tachiyomi version: ? | ||||
| * Android version: ? | ||||
| * Device: ? | ||||
|  | ||||
| ## Steps to reproduce | ||||
| 1. First step | ||||
|   | ||||
							
								
								
									
										12
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/ISSUE_TEMPLATE/feature_request.md
									
									
									
									
										vendored
									
									
								
							| @@ -3,8 +3,20 @@ name: "🌟 Feature request" | ||||
| about: Suggest a feature to improve Tachiyomi | ||||
| title: "[Feature Request] Write short description here" | ||||
| labels: "feature" | ||||
| --- | ||||
|  | ||||
| **PLEASE READ THIS** | ||||
|  | ||||
| I acknowledge that: | ||||
|  | ||||
| - I have updated to the latest version of the app (stable is v0.9.2) | ||||
| - I have updated all extensions | ||||
| - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions | ||||
|  | ||||
| **DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT** | ||||
|  | ||||
| --- | ||||
|  | ||||
| ### Why/User Benefit/User Problem | ||||
| (explain why this feature should be added) | ||||
|  | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/source_issue.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/source_issue.md
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| --- | ||||
| name: "Extension/source/catalogue issue" | ||||
| about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions" | ||||
| title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions" | ||||
| labels: "catalog" | ||||
| --- | ||||
|  | ||||
| DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions | ||||
							
								
								
									
										
											BIN
										
									
								
								.github/readme-images/screens.png
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								.github/readme-images/screens.png
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB | 
							
								
								
									
										13
									
								
								.github/workflows/issue_closer.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								.github/workflows/issue_closer.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| name: Issue closer | ||||
| on: [issues] | ||||
| jobs: | ||||
|   autoclose: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|     - name: Autoclose issue | ||||
|       uses: arkon/issue-closer-action@v1.0 | ||||
|       with: | ||||
|         repo-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|         issue-close-message: "@${issue.user.login} this issue was automatically closed because it was not filled in correctly or the acknowledgment section was not removed." | ||||
|         issue-title-pattern: ".*THIS ISSUE IS IN THE WRONG REPO.*" | ||||
|         issue-body-pattern: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*" | ||||
							
								
								
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -2,14 +2,21 @@ | ||||
| /local.properties | ||||
| /.idea/workspace.xml | ||||
| .DS_Store | ||||
| /build | ||||
| .idea/ | ||||
| *iml | ||||
| *.iml | ||||
| */build | ||||
| /mainframer | ||||
| /.mainframer | ||||
|  | ||||
| # Built files | ||||
| */build | ||||
| /build | ||||
| *.apk | ||||
| app/**/output.json | ||||
|  | ||||
| # Hebrew assets are copied on build | ||||
| app/src/main/res/values-iw/ | ||||
|  | ||||
| TODO.md | ||||
| CHANGELOG.md | ||||
| /captures | ||||
|   | ||||
							
								
								
									
										101
									
								
								.travis.yml
									
									
									
									
									
								
							
							
						
						
									
										101
									
								
								.travis.yml
									
									
									
									
									
								
							| @@ -1,59 +1,74 @@ | ||||
| dist: trusty | ||||
| language: android | ||||
|  | ||||
| android: | ||||
|   components: | ||||
|   - build-tools-29.0.2 | ||||
|   - android-28 | ||||
|   - extra-android-m2repository | ||||
|   - extra-google-m2repository | ||||
|   - extra-android-support | ||||
|   - extra-google-google_play_services | ||||
|     - tools | ||||
|     - platform-tools | ||||
|     - build-tools-29.0.3 | ||||
|     - android-29 | ||||
|     - extra-android-m2repository | ||||
|     - extra-google-m2repository | ||||
|     - extra-android-support | ||||
|     - extra-google-google_play_services | ||||
|  | ||||
|   licenses: | ||||
|   - android-sdk-license-.+ | ||||
|     - 'android-sdk-license-.+' | ||||
|     - 'android-sdk-preview-license-.+' | ||||
|  | ||||
| before_install: | ||||
| - yes | sdkmanager "platforms;android-28" # workaround for accepting the license | ||||
| - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then | ||||
|   - yes | sdkmanager "platforms;android-29" # workaround for accepting the license | ||||
|   - if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then | ||||
|     openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d; | ||||
|     tar xf secrets.tar; | ||||
|     mv debug.keystore "$HOME/.android"; | ||||
|   fi | ||||
| - git clone https://github.com/urho3d/android-ndk.git $HOME/android-ndk-root | ||||
| - export ANDROID_NDK_HOME=$HOME/android-ndk-root | ||||
| - mkdir "$ANDROID_HOME/licenses" || true | ||||
| - echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license" | ||||
| - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license" | ||||
|     fi | ||||
|   - mkdir "$ANDROID_HOME/licenses" || true | ||||
|   - echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_HOME/licenses/android-sdk-license" | ||||
|   - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license" | ||||
|  | ||||
| install: | ||||
|   - echo y | sdkmanager "ndk-bundle" | ||||
|  | ||||
| before_script: | ||||
|   - export ANDROID_NDK_HOME=$ANDROID_HOME/ndk-bundle | ||||
|  | ||||
| script: ".travis/build.sh" | ||||
|  | ||||
| before_cache: | ||||
| - rm -f  $HOME/.gradle/caches/modules-2/modules-2.lock | ||||
| - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ | ||||
|   - rm -f  $HOME/.gradle/caches/modules-2/modules-2.lock | ||||
|   - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ | ||||
|  | ||||
| cache: | ||||
|   directories: | ||||
|   - "$HOME/.gradle/caches/" | ||||
|   - "$HOME/.gradle/wrapper/" | ||||
|   - "$HOME/.android/build-cache" | ||||
|     - "$HOME/.gradle/caches/" | ||||
|     - "$HOME/.gradle/wrapper/" | ||||
|     - "$HOME/.android/build-cache" | ||||
|  | ||||
| deploy: | ||||
| - provider: releases | ||||
|   api_key: | ||||
|     secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA= | ||||
|   file: tachiyomi-v*.apk | ||||
|   file_glob: true | ||||
|   skip_cleanup: true | ||||
|   on: | ||||
|     tags: true | ||||
|     repo: inorichi/tachiyomi | ||||
| - provider: script | ||||
|   script: ".travis/deploy.sh" | ||||
|   skip_cleanup: true | ||||
|   on: | ||||
|     branch: master | ||||
|     condition: "-z $TRAVIS_TAG" | ||||
|     repo: inorichi/tachiyomi | ||||
|   - provider: releases | ||||
|     api_key: | ||||
|       secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA= | ||||
|     file: tachiyomi-v*.apk | ||||
|     file_glob: true | ||||
|     skip_cleanup: true | ||||
|     on: | ||||
|       tags: true | ||||
|       repo: inorichi/tachiyomi | ||||
|   - provider: script | ||||
|     script: ".travis/deploy.sh" | ||||
|     skip_cleanup: true | ||||
|     on: | ||||
|       branch: master | ||||
|       condition: "-z $TRAVIS_TAG" | ||||
|       repo: inorichi/tachiyomi | ||||
|  | ||||
| env: | ||||
|   global: | ||||
|   - secure: Ita1+tzo7P5IC2yqU3KgRcXt+5DTpP0103Hx/ECYi42/7rLt+TC7PMjl2yH3Z189+tGwLq0Ol1KJ2Z5Rn3q7EaQgD0+WRkH/ijtrjKoVh7dyItIBp7yowZpA0TJHQ4EZpGSxZakKbIP4di8XMxJ2+5VzEivYUt04LCUpzugemL6b6XOfUmOZykVxV2UDAlPPggklITYBXkHUa0mwJhjS1aPPeeR3PhVXomkqfuOJOKejPXXXJope9fhAnmopHA7ISfjIrTuwDVQJqNSuco+O9kQShmlu0C8pob1vFGPEDvafaDS8SZ9A6gKT1ZfgSUqVmvDbx0WLX8XugBLrQedtZv63esOa1WUyGhgFVpeJjexlszXlhyfP1gH5QbzRr+EiSaagCyjf9II2veLAtU5cFY+nj6KBdKQsazIMRHf8SAQlWASyJYMED/N9RnUFxSf1rnLGqiY2ezjycx4ieFj7vhlbTgyao1GHjjR9cwNuntdMYWhY8+Vc7Fctmzm46xOyyz9oJGdyim76Y4w4MZvQNKeZOBAjdEgX6cXBk15scoM2Vj9ENox+MKZLaKRawXg2U1ujK+bWAQkXiVvPriv05/JtYsNUft8qAsm+69vtohDsUW7Wu0bBIKDL+v0W30ty1PpyNehBB2OquUE7fp53gitOmYl7TyuxktkMY8PXKKU= | ||||
|   - secure: NABCfigMUVM/9TLALYBpQLp/p3rG6MbH5y34/oqCSej/oh2u0nyhFSi8veS0lFpDIcv0TZvxHJXoSA0zeZijb1fUU8jYVNT2azuPWE6Gu7sf0TfBeCvulqbgLMoaA6JuWbEbZwHcxpKHg4vLSMjNk+ZP4v2dffI6A620fxLltxxhTpsYkYYsfKG857CpQtdgN/HqcOaxyvzXFmWWyVWHala1uMcMeXZCwgnlVAqau9o0bsU092txSmHqoesHoAinidSCTCmTlEqp/1AFaYQTbxmnfNC1yLgzxWAlJcS3NWzNo3ellMvKjsiIGn3JJpAjTGcyf3yPsvhs1cY3MZbmJYVyKH5HbnkA5ms6mx0DDJ2UOs5H2dmED82m14+hu62Xb8XN8zAdq+bySNSwgsGzvr1PT74pT4BW1T+D7L1xvUe6k1enZ38GIMJbJPhBybRQazhjKPmXRB30Thxoqe5VqU8UeiXHAEps7JYAWUR1PLZvEYQb6MWurmTxs9be/OTwrUT1LDiqeJZg6XkDGgQwuR2YBaQJHJD17Piq6q1BUX8abhK6wzAAYVqyGvpmUCmQCtHZgenE6ulwcKChzBv4n97OjE21LGWnbNF5ViUhfAbGcKOVufd1chZsfbkJ7a3tHYCfLnxHUIhKvHk26f5Em8h68D0wQkPnkcVVkfh7XpI= | ||||
|   - secure: C93UADV5aR0zhLCLwR6tCyz+fwUYslZbhjBl3PHQp+0boIGS/Be2UQTFHp/NB9mQmhWqbwqHoAVFENZFytV04ePgOuNtMFcjAIfnzm19Am7iRAMFixD45pF/CuYYfLupElkAcSequtAzN0g4G0sQ5KR1hibaDIoz9kfA2YcUAMaZ4T5bhCr8os/xA2nOlmvPDWsRWCFBYkSpnmbsSsgYAhulA/V5bSNAWnp9LPw3CBLibW3WsVP4wuhZAkXznKwn/mHT31kfQlpMH3qNhXpsN9huUkZ/k8QWeakcHJKugung0Z2T1StK8rlI8OrJstVcwueHTa2ses4f5VbhWog/Z8HDkdll9W9RM/QqXjNDtOVBt/SPuhCp4k2rvJixFUxzvSqgSWQvQnbHwjWxIUVVyHtnb0/zc/S9ONZG14TOwB/+Lkgacb85PNszurZ2f3mH0O6slIh1mH+5d9J4+L976ot4nTPlK1OtothagVyKGOrn9HycrPk/MjftIJuElHzo7NEJd/wRPqIb5y12iZN1RSPriU+itg1uSAVP891/o3peJyuqY9WSB7dYwgDJos6dDvbr19emtdyxkQR+eAb5duyH6s4R58wh1kJ1d4zu0X6uSnF4AIc+6teKkN24rSXcqB/hrcojS49jgLy5P0/CVsUbYZPI/tx8E/IJfr8m36E= | ||||
|   - secure: mawwBvllvESc/mp+JHvncq1iUhiC7nyokPgXmOehffc0K3byMLs2L25HjNsU6EnXG9Lcae1cfP8S9bWLquU2C3kpAkLBUpjEbdx7K0654uvs7Rrvb5hcTRHwjzaEVmVaBFX4ROcjUhBYny/Wjj/YENCkSWpkfcMd1esFbVsO+fOLyaAPvrb6auKY7H+pUSqlEwaEnrkYeBBZIHa7KqwL4g5DHbq6K368tjmval/wBzwMB0V8V3dt/ik8RMVDtKPrik4Bu0V9UmXZUIo/a06ii/CM82ekFRh3eUb0DKkdkmYbdH6MBMoLTfQtMa6A4luXaA0oycAnTX3OGB5MWIjK39KhWRavh6ybSIt4aHKoolxzH8Zgmk7xMhFSot/laX5q5IzjZu5KU6F2SmdV0kcQugM8oAjANFySetPvY1q7nZ8pM+NO1xKS/mH0w4vChhdJFD1mw7aCoh8FdeUf0Eym2+pp5Q9uAisWMmNn5XN8/fL5q6PzAxkXmkedfrr1N61FmIL6EKx8qiWpOUNlRRTIMJ4GMhCyckCF6cNxDkBItp52c+Hmkbn+ZEInEyX6gpjYVm3xyEi0Z5kLCi/fMX2nBNczc5BuGLzzmJnITv4ovpeYn2/vPvHbaPgPC4LqDK3AjlpVadMZk/M5Egn+hWY7Mni57CmpZD+SpxUbbsItI0c= | ||||
|   - secure: PJPDkUg1zc57brxUvNpSh+Q3ZEaGpBqZzwDavqslkn0WmjBTLrE6/OG7TFHKNmO+P56qFl+pMEKqThxqR3+4bWEeEx8ykkixDVzxNJMmws+7A7ImJ75iQyB6giMW/4tykVMMHgIPNAdcnI8VOWn0LGHnpFWUd70yoyAGX8s6cspHCKgcuWMA3GS410KJfHpyd0B9/QS7ZyWzSETW7zSPyLPa81SBO95EhOF3TOGZYLt/mBhdtU3YGFs4k9fZ8jDDcm9XmBfqVlUhb8HiZcxJiZDdRvxODERfNnwc47uaJk6+kxGDzIW2uAxrMXXVKkG04GeMOokXoR9kW1Hl2JmoyySLKLZmB7I/XEtVWdzZw16mWi+4zmhjLhfB0phSW+/5I+0VtZZ6jO031J5FL/JqVrcq1ws/aw4QlaOdPUco/x2u4LNHyYYgOi5arD9xSyu6IRy0jCC4Xa1zuqM5adGJX+rZyVfKZ0TxOW661HTxlo8COtkB2i0WR2deZGVN75ooCAEO8DauQoUcFH1OelahmPtzVs1/6ZczuxGdp9ED7ZQq9NHEOsOdUGCj/D79Dm1hWFQsIsslnnGYWitAycNCgEwmlt2Q6fbrv2CJrmLqZ9a9r3AhzxoHn9Qx1GyuyfhZJzm/6Ff2kcOjma2kcz13KUwTxdW+2G5dDCotK3f7aiI= | ||||
|   - secure: FIIZfEEYfjNMKODs33Czh603CYVn6LRrzpFNIiPHYTb8iQWv9qAYhsg4FpHfOjDikokTwb5X/h8G7AX93Z0xKyyDi75ACT11oPeTNTArDdcmdDVlOYBvYHc2Ci7pMW5r8LGejB7Y3mWM8uKyA3oKvneEFutB65vO3JVZvFWrm03Lmqqe7+mA4qNqNqTbN7R7fmk5b7zt7A3DHvDu0JPTbSSUwpso/p2I5WJYjrf71I7YMQwIFLoMfplC1onVA3EFS3lZsF65zE+xVRy34AKa41iZAMbhVDyqUHEnx6L0dwEdn2Z5XLlK0ov1+qLTLlQsBE4Knre6TNkWMfktk7MKA+ch8RYxvEYLODhQkIrOkLSNWhZPhdaT+xD4fr0RCKSHo6uWRC4aofsJx8wSqb8ZL4j2zopUp9VisMOI202UEnvFDBtOkVGJSxxYbFjifIB7NCJBn788w+3k+k4IbOg537VdyoK2PMBR8/TDdjImWhWHY1i7+345ejwmzHL7ZPfb6GTNnQTWkajT77/n6Yk41twR5vvegOSTKuuO++WN/pUks4PGqtcQe9fnSfx2OcOq1ofLiG+JDorJ7z8kHSG13wHLq+QYMDayQbyJEYpDzmn/w3Ou1s2o0a7A41+cIkRzAgH9y3v4lgjp9GcMP2S74ZPA7OecWbFSexM7tL/dYxY= | ||||
|   - secure: DKCGc4E9PKeTX68r9pbbNg5qITsN0bApQ1m0x8xdEoi8GLRKVMYNn6ahoAxvy1YsBXC9Zlt5++gLmUV1I1JyDMyJXMr/lZrp4oarW0xWpTBmn3HzOph/K2W4i/fTGgMFieumPEbQIFOnU3JSjK6UJB8qVGEXD2OqS7A//EdrGDbAYVDL3ZTKE6JUlTNHgaKaNHhn+Dq4aBLTSYPwlLyqo+WNBVUUCKCHOq62ULF8MpX5YGaPFNxKYzircV7HpF1hCbV31dmpkeYT9xztra5V0SIBM27jAcQqGmtHH2mhx1sLu+gjhFJbbtY6cggA9EedzYYLDx/NPmgfyuOJfyVbSwTF3vhDUYfskqc1THWpwOSKO0Ry+8/xYb9crxg+FSwuI5hnfkIFk9woBvRGBhjto3/1buMNY9dSFiWtEbN6Let8e747l0wIGJCpJxSeh7vn7F1mWjixhf9GX1+V9BrUvGTd3XJDNb9cVnafYa1RTj8BLteA4HBza7Z9R3dvG4YWp16L/94UuaTzgAQfERLTZGopQth/hsaVTlYesJmJLF70lGM+W83y3YuNkSaX1zQ5FAIvp7oH0O16t7ISm6GprUFwN2Uox7AAbPZdWHxJbly+D+yCFNcqS3Bz9mV3YCLo690Sy1ePNHr+nCseVfBMo7OYyavSS/EjPWfEy65Wq04= | ||||
|     - secure: Ita1+tzo7P5IC2yqU3KgRcXt+5DTpP0103Hx/ECYi42/7rLt+TC7PMjl2yH3Z189+tGwLq0Ol1KJ2Z5Rn3q7EaQgD0+WRkH/ijtrjKoVh7dyItIBp7yowZpA0TJHQ4EZpGSxZakKbIP4di8XMxJ2+5VzEivYUt04LCUpzugemL6b6XOfUmOZykVxV2UDAlPPggklITYBXkHUa0mwJhjS1aPPeeR3PhVXomkqfuOJOKejPXXXJope9fhAnmopHA7ISfjIrTuwDVQJqNSuco+O9kQShmlu0C8pob1vFGPEDvafaDS8SZ9A6gKT1ZfgSUqVmvDbx0WLX8XugBLrQedtZv63esOa1WUyGhgFVpeJjexlszXlhyfP1gH5QbzRr+EiSaagCyjf9II2veLAtU5cFY+nj6KBdKQsazIMRHf8SAQlWASyJYMED/N9RnUFxSf1rnLGqiY2ezjycx4ieFj7vhlbTgyao1GHjjR9cwNuntdMYWhY8+Vc7Fctmzm46xOyyz9oJGdyim76Y4w4MZvQNKeZOBAjdEgX6cXBk15scoM2Vj9ENox+MKZLaKRawXg2U1ujK+bWAQkXiVvPriv05/JtYsNUft8qAsm+69vtohDsUW7Wu0bBIKDL+v0W30ty1PpyNehBB2OquUE7fp53gitOmYl7TyuxktkMY8PXKKU= | ||||
|     - secure: NABCfigMUVM/9TLALYBpQLp/p3rG6MbH5y34/oqCSej/oh2u0nyhFSi8veS0lFpDIcv0TZvxHJXoSA0zeZijb1fUU8jYVNT2azuPWE6Gu7sf0TfBeCvulqbgLMoaA6JuWbEbZwHcxpKHg4vLSMjNk+ZP4v2dffI6A620fxLltxxhTpsYkYYsfKG857CpQtdgN/HqcOaxyvzXFmWWyVWHala1uMcMeXZCwgnlVAqau9o0bsU092txSmHqoesHoAinidSCTCmTlEqp/1AFaYQTbxmnfNC1yLgzxWAlJcS3NWzNo3ellMvKjsiIGn3JJpAjTGcyf3yPsvhs1cY3MZbmJYVyKH5HbnkA5ms6mx0DDJ2UOs5H2dmED82m14+hu62Xb8XN8zAdq+bySNSwgsGzvr1PT74pT4BW1T+D7L1xvUe6k1enZ38GIMJbJPhBybRQazhjKPmXRB30Thxoqe5VqU8UeiXHAEps7JYAWUR1PLZvEYQb6MWurmTxs9be/OTwrUT1LDiqeJZg6XkDGgQwuR2YBaQJHJD17Piq6q1BUX8abhK6wzAAYVqyGvpmUCmQCtHZgenE6ulwcKChzBv4n97OjE21LGWnbNF5ViUhfAbGcKOVufd1chZsfbkJ7a3tHYCfLnxHUIhKvHk26f5Em8h68D0wQkPnkcVVkfh7XpI= | ||||
|     - secure: C93UADV5aR0zhLCLwR6tCyz+fwUYslZbhjBl3PHQp+0boIGS/Be2UQTFHp/NB9mQmhWqbwqHoAVFENZFytV04ePgOuNtMFcjAIfnzm19Am7iRAMFixD45pF/CuYYfLupElkAcSequtAzN0g4G0sQ5KR1hibaDIoz9kfA2YcUAMaZ4T5bhCr8os/xA2nOlmvPDWsRWCFBYkSpnmbsSsgYAhulA/V5bSNAWnp9LPw3CBLibW3WsVP4wuhZAkXznKwn/mHT31kfQlpMH3qNhXpsN9huUkZ/k8QWeakcHJKugung0Z2T1StK8rlI8OrJstVcwueHTa2ses4f5VbhWog/Z8HDkdll9W9RM/QqXjNDtOVBt/SPuhCp4k2rvJixFUxzvSqgSWQvQnbHwjWxIUVVyHtnb0/zc/S9ONZG14TOwB/+Lkgacb85PNszurZ2f3mH0O6slIh1mH+5d9J4+L976ot4nTPlK1OtothagVyKGOrn9HycrPk/MjftIJuElHzo7NEJd/wRPqIb5y12iZN1RSPriU+itg1uSAVP891/o3peJyuqY9WSB7dYwgDJos6dDvbr19emtdyxkQR+eAb5duyH6s4R58wh1kJ1d4zu0X6uSnF4AIc+6teKkN24rSXcqB/hrcojS49jgLy5P0/CVsUbYZPI/tx8E/IJfr8m36E= | ||||
|     - secure: mawwBvllvESc/mp+JHvncq1iUhiC7nyokPgXmOehffc0K3byMLs2L25HjNsU6EnXG9Lcae1cfP8S9bWLquU2C3kpAkLBUpjEbdx7K0654uvs7Rrvb5hcTRHwjzaEVmVaBFX4ROcjUhBYny/Wjj/YENCkSWpkfcMd1esFbVsO+fOLyaAPvrb6auKY7H+pUSqlEwaEnrkYeBBZIHa7KqwL4g5DHbq6K368tjmval/wBzwMB0V8V3dt/ik8RMVDtKPrik4Bu0V9UmXZUIo/a06ii/CM82ekFRh3eUb0DKkdkmYbdH6MBMoLTfQtMa6A4luXaA0oycAnTX3OGB5MWIjK39KhWRavh6ybSIt4aHKoolxzH8Zgmk7xMhFSot/laX5q5IzjZu5KU6F2SmdV0kcQugM8oAjANFySetPvY1q7nZ8pM+NO1xKS/mH0w4vChhdJFD1mw7aCoh8FdeUf0Eym2+pp5Q9uAisWMmNn5XN8/fL5q6PzAxkXmkedfrr1N61FmIL6EKx8qiWpOUNlRRTIMJ4GMhCyckCF6cNxDkBItp52c+Hmkbn+ZEInEyX6gpjYVm3xyEi0Z5kLCi/fMX2nBNczc5BuGLzzmJnITv4ovpeYn2/vPvHbaPgPC4LqDK3AjlpVadMZk/M5Egn+hWY7Mni57CmpZD+SpxUbbsItI0c= | ||||
|     - secure: PJPDkUg1zc57brxUvNpSh+Q3ZEaGpBqZzwDavqslkn0WmjBTLrE6/OG7TFHKNmO+P56qFl+pMEKqThxqR3+4bWEeEx8ykkixDVzxNJMmws+7A7ImJ75iQyB6giMW/4tykVMMHgIPNAdcnI8VOWn0LGHnpFWUd70yoyAGX8s6cspHCKgcuWMA3GS410KJfHpyd0B9/QS7ZyWzSETW7zSPyLPa81SBO95EhOF3TOGZYLt/mBhdtU3YGFs4k9fZ8jDDcm9XmBfqVlUhb8HiZcxJiZDdRvxODERfNnwc47uaJk6+kxGDzIW2uAxrMXXVKkG04GeMOokXoR9kW1Hl2JmoyySLKLZmB7I/XEtVWdzZw16mWi+4zmhjLhfB0phSW+/5I+0VtZZ6jO031J5FL/JqVrcq1ws/aw4QlaOdPUco/x2u4LNHyYYgOi5arD9xSyu6IRy0jCC4Xa1zuqM5adGJX+rZyVfKZ0TxOW661HTxlo8COtkB2i0WR2deZGVN75ooCAEO8DauQoUcFH1OelahmPtzVs1/6ZczuxGdp9ED7ZQq9NHEOsOdUGCj/D79Dm1hWFQsIsslnnGYWitAycNCgEwmlt2Q6fbrv2CJrmLqZ9a9r3AhzxoHn9Qx1GyuyfhZJzm/6Ff2kcOjma2kcz13KUwTxdW+2G5dDCotK3f7aiI= | ||||
|     - secure: FIIZfEEYfjNMKODs33Czh603CYVn6LRrzpFNIiPHYTb8iQWv9qAYhsg4FpHfOjDikokTwb5X/h8G7AX93Z0xKyyDi75ACT11oPeTNTArDdcmdDVlOYBvYHc2Ci7pMW5r8LGejB7Y3mWM8uKyA3oKvneEFutB65vO3JVZvFWrm03Lmqqe7+mA4qNqNqTbN7R7fmk5b7zt7A3DHvDu0JPTbSSUwpso/p2I5WJYjrf71I7YMQwIFLoMfplC1onVA3EFS3lZsF65zE+xVRy34AKa41iZAMbhVDyqUHEnx6L0dwEdn2Z5XLlK0ov1+qLTLlQsBE4Knre6TNkWMfktk7MKA+ch8RYxvEYLODhQkIrOkLSNWhZPhdaT+xD4fr0RCKSHo6uWRC4aofsJx8wSqb8ZL4j2zopUp9VisMOI202UEnvFDBtOkVGJSxxYbFjifIB7NCJBn788w+3k+k4IbOg537VdyoK2PMBR8/TDdjImWhWHY1i7+345ejwmzHL7ZPfb6GTNnQTWkajT77/n6Yk41twR5vvegOSTKuuO++WN/pUks4PGqtcQe9fnSfx2OcOq1ofLiG+JDorJ7z8kHSG13wHLq+QYMDayQbyJEYpDzmn/w3Ou1s2o0a7A41+cIkRzAgH9y3v4lgjp9GcMP2S74ZPA7OecWbFSexM7tL/dYxY= | ||||
|     - secure: DKCGc4E9PKeTX68r9pbbNg5qITsN0bApQ1m0x8xdEoi8GLRKVMYNn6ahoAxvy1YsBXC9Zlt5++gLmUV1I1JyDMyJXMr/lZrp4oarW0xWpTBmn3HzOph/K2W4i/fTGgMFieumPEbQIFOnU3JSjK6UJB8qVGEXD2OqS7A//EdrGDbAYVDL3ZTKE6JUlTNHgaKaNHhn+Dq4aBLTSYPwlLyqo+WNBVUUCKCHOq62ULF8MpX5YGaPFNxKYzircV7HpF1hCbV31dmpkeYT9xztra5V0SIBM27jAcQqGmtHH2mhx1sLu+gjhFJbbtY6cggA9EedzYYLDx/NPmgfyuOJfyVbSwTF3vhDUYfskqc1THWpwOSKO0Ry+8/xYb9crxg+FSwuI5hnfkIFk9woBvRGBhjto3/1buMNY9dSFiWtEbN6Let8e747l0wIGJCpJxSeh7vn7F1mWjixhf9GX1+V9BrUvGTd3XJDNb9cVnafYa1RTj8BLteA4HBza7Z9R3dvG4YWp16L/94UuaTzgAQfERLTZGopQth/hsaVTlYesJmJLF70lGM+W83y3YuNkSaX1zQ5FAIvp7oH0O16t7ISm6GprUFwN2Uox7AAbPZdWHxJbly+D+yCFNcqS3Bz9mV3YCLo690Sy1ePNHr+nCseVfBMo7OYyavSS/EjPWfEy65Wq04= | ||||
|   | ||||
| @@ -1,3 +1 @@ | ||||
| I dont want to maintain a readme | ||||
|  | ||||
| #YEET | ||||
| I havent started a readme | ||||
							
								
								
									
										144
									
								
								app/build.gradle
									
									
									
									
									
								
							
							
						
						
									
										144
									
								
								app/build.gradle
									
									
									
									
									
								
							| @@ -2,6 +2,7 @@ | ||||
| import java.text.SimpleDateFormat | ||||
|  | ||||
| apply plugin: 'com.android.application' | ||||
| apply plugin: 'com.google.android.gms.oss-licenses-plugin' | ||||
| apply plugin: 'kotlin-android' | ||||
| apply plugin: 'kotlin-android-extensions' | ||||
| apply plugin: 'kotlin-kapt' | ||||
| @@ -32,25 +33,23 @@ ext { | ||||
| } | ||||
|  | ||||
| android { | ||||
|     compileSdkVersion 28 | ||||
|     buildToolsVersion '29.0.2' | ||||
|     compileSdkVersion 29 | ||||
|     buildToolsVersion '29.0.3' | ||||
|     publishNonDefault true | ||||
|  | ||||
|     defaultConfig { | ||||
|         applicationId "eu.kanade.tachiyomi.az" | ||||
|         minSdkVersion 16 | ||||
|         targetSdkVersion 28 | ||||
|         applicationId "eu.kanade.tachiyomi.sy" | ||||
|         minSdkVersion 21 | ||||
|         targetSdkVersion 29 | ||||
|         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | ||||
|         versionCode 8405 | ||||
|         versionName "v8.4.5-AZ" | ||||
|         versionCode 1 | ||||
|         versionName "v0.9.2.0" | ||||
|  | ||||
|         buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" | ||||
|         buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" | ||||
|         buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\"" | ||||
|         buildConfigField "boolean", "INCLUDE_UPDATER", "true" | ||||
|  | ||||
|         vectorDrawables.useSupportLibrary = true | ||||
|  | ||||
|         multiDexEnabled true | ||||
|  | ||||
|         ndk { | ||||
| @@ -58,6 +57,10 @@ android { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     viewBinding { | ||||
|         enabled = true | ||||
|     } | ||||
|  | ||||
|     buildTypes { | ||||
|         debug { | ||||
|             versionNameSuffix "-${getCommitCount()}" | ||||
| @@ -72,8 +75,8 @@ android { | ||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||||
|         } | ||||
|         release { | ||||
| //            minifyEnabled true | ||||
| //            shrinkResources true | ||||
|             minifyEnabled true | ||||
|             shrinkResources true | ||||
|             zipAlignEnabled true | ||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||||
|         } | ||||
| @@ -127,6 +130,10 @@ android { | ||||
|     } | ||||
| } | ||||
|  | ||||
| androidExtensions { | ||||
|     experimental = true | ||||
| } | ||||
|  | ||||
| dependencies { | ||||
|  | ||||
|     // Modified dependencies | ||||
| @@ -134,41 +141,46 @@ dependencies { | ||||
|     implementation 'com.github.inorichi:junrar-android:634c1f5' | ||||
|  | ||||
|     // Android support library | ||||
|     implementation 'androidx.legacy:legacy-support-v4:1.0.0' | ||||
|     implementation 'androidx.appcompat:appcompat:1.1.0' | ||||
|     implementation 'androidx.cardview:cardview:1.0.0' | ||||
|     implementation 'com.google.android.material:material:1.0.0' | ||||
|     implementation 'androidx.constraintlayout:constraintlayout:1.1.3' | ||||
|     implementation 'androidx.recyclerview:recyclerview:1.1.0' | ||||
|     implementation 'androidx.preference:preference:1.1.0' | ||||
|     implementation 'androidx.preference:preference:1.1.1' | ||||
|     implementation 'androidx.annotation:annotation:1.1.0' | ||||
|     implementation 'androidx.browser:browser:1.2.0' | ||||
|  | ||||
|     implementation 'androidx.constraintlayout:constraintlayout:1.1.3' | ||||
|  | ||||
|     implementation 'androidx.multidex:multidex:2.0.1' | ||||
|     implementation 'androidx.biometric:biometric:1.0.1' | ||||
|  | ||||
|     // DO NOT UPGRADE TO 17.0, IT REQUIRES ANDROIDX | ||||
|     standardImplementation 'com.google.firebase:firebase-core:17.2.1' | ||||
|     final lifecycle_version = '2.2.0' | ||||
|     implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" | ||||
|     implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" | ||||
|     implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" | ||||
|  | ||||
|     // UI library | ||||
|     implementation 'com.google.android.material:material:1.1.0' | ||||
|  | ||||
|     standardImplementation 'com.google.firebase:firebase-core:17.4.0' | ||||
|  | ||||
|     // ReactiveX | ||||
|     implementation 'io.reactivex:rxandroid:1.2.1' | ||||
|     implementation 'io.reactivex:rxjava:1.3.8' | ||||
|     implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0' | ||||
|     implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2' | ||||
|     implementation 'com.github.pwittchen:reactivenetwork:0.13.0' | ||||
|  | ||||
|     // Network client | ||||
|     implementation "com.squareup.okhttp3:okhttp:4.2.1" // DO NOT UPGRADE TO 3.13.X+, it requires minSdk 21 | ||||
|     implementation 'com.squareup.okio:okio:2.4.0' // I think we can do 2.x, okhttp is ok with it but is there any other deps that need 1.x? | ||||
|     final okhttp_version = '4.5.0' | ||||
|     implementation "com.squareup.okhttp3:okhttp:$okhttp_version" | ||||
|     implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" | ||||
|     implementation 'com.squareup.okio:okio:2.6.0' | ||||
|  | ||||
|     // REST | ||||
|     final retrofit_version = '2.6.2' | ||||
|     final retrofit_version = '2.8.1' | ||||
|     implementation "com.squareup.retrofit2:retrofit:$retrofit_version" | ||||
|     implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" | ||||
|     implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version" | ||||
|  | ||||
|     // JSON | ||||
|     implementation 'com.google.code.gson:gson:2.8.5' | ||||
|     implementation 'com.google.code.gson:gson:2.8.6' | ||||
|     implementation 'com.github.salomonbrys.kotson:kotson:2.5.0' | ||||
|  | ||||
|     // JavaScript engine | ||||
| @@ -179,12 +191,12 @@ dependencies { | ||||
|     implementation 'com.github.inorichi:unifile:e9ee588' | ||||
|  | ||||
|     // HTML parser | ||||
|     implementation 'org.jsoup:jsoup:1.12.1' | ||||
|     implementation 'org.jsoup:jsoup:1.13.1' | ||||
|  | ||||
|     // Job scheduling | ||||
|     implementation 'com.evernote:android-job:1.2.5' | ||||
|     // DO NOT UPGRADE TO 17.0, IT REQUIRES ANDROIDX | ||||
|     implementation 'com.google.android.gms:play-services-gcm:17.0.0' | ||||
|     final work_version = '2.3.4' | ||||
|     implementation "androidx.work:work-runtime:$work_version" | ||||
|     implementation "androidx.work:work-runtime-ktx:$work_version" | ||||
|  | ||||
|     // [EXH] Android 7 SSL Workaround | ||||
|     implementation 'com.google.android.gms:play-services-safetynet:17.0.0' | ||||
| @@ -193,13 +205,17 @@ dependencies { | ||||
|     implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0' | ||||
|  | ||||
|     // Database | ||||
|     implementation 'androidx.sqlite:sqlite:2.0.1' | ||||
|     implementation 'androidx.sqlite:sqlite:2.1.0' | ||||
|     implementation 'com.github.inorichi.storio:storio-common:8be19de@aar' | ||||
|     implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar' | ||||
|     implementation 'io.requery:sqlite-android:3.25.2' | ||||
|     implementation 'io.requery:sqlite-android:3.31.0' | ||||
|  | ||||
|     // Preferences | ||||
|     implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2' | ||||
|     implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1' | ||||
|  | ||||
|     // Model View Presenter | ||||
|     final nucleus_version = '3.0.1' | ||||
|     final nucleus_version = '6.0.0' | ||||
|     implementation "info.android15.nucleus:nucleus:$nucleus_version" | ||||
|     implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version" | ||||
|  | ||||
| @@ -212,47 +228,51 @@ dependencies { | ||||
|     implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version" | ||||
|     kapt "com.github.bumptech.glide:compiler:$glide_version" | ||||
|  | ||||
|     // Transformations | ||||
|     implementation 'jp.wasabeef:glide-transformations:4.0.0' | ||||
|  | ||||
|     // Logging | ||||
|     implementation 'com.jakewharton.timber:timber:4.7.1' | ||||
|  | ||||
|     // Crash reports | ||||
|     implementation 'ch.acra:acra:4.9.2' | ||||
|     final acra_version = '5.5.0' | ||||
|     implementation "ch.acra:acra-http:$acra_version" | ||||
|  | ||||
|     // Sort | ||||
|     implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' | ||||
|  | ||||
|     // UI | ||||
|     implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4' | ||||
|     implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' | ||||
|     implementation 'eu.davidea:flexible-adapter:5.1.0' // Cannot upgrade to 5.1.0 as it uses AndroidX | ||||
|     implementation 'eu.davidea:flexible-adapter:5.1.0' | ||||
|     implementation 'eu.davidea:flexible-adapter-ui:1.0.0' | ||||
|     implementation 'com.nononsenseapps:filepicker:2.5.2' | ||||
|     implementation 'com.github.amulyakhare:TextDrawable:558677e' | ||||
|     implementation 'com.afollestad.material-dialogs:core:0.9.6.0' // Cannot upgrade to 2.x, AndroidX and API changes | ||||
|     implementation 'me.zhanghai.android.systemuihelper:library:1.0.0' | ||||
|     implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0' | ||||
|     implementation 'com.github.mthli:Slice:v1.3' | ||||
|     implementation 'me.gujun.android.taggroup:library:1.4@aar' | ||||
|     implementation 'com.github.chrisbanes:PhotoView:2.3.0' // Cannot upgrade to 2.2.x+ as it uses AndroidX | ||||
|     implementation 'com.github.chrisbanes:PhotoView:2.3.0' | ||||
|     implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a' | ||||
|  | ||||
|     // 3.2.0+ introduces weird UI blinking or cut off issues on some devices | ||||
|     final material_dialogs_version = '3.1.1' | ||||
|     implementation "com.afollestad.material-dialogs:core:$material_dialogs_version" | ||||
|     implementation "com.afollestad.material-dialogs:input:$material_dialogs_version" | ||||
|     implementation "com.afollestad.material-dialogs:datetime:$material_dialogs_version" | ||||
|  | ||||
|     // Conductor | ||||
|     implementation 'com.bluelinelabs:conductor:2.1.5' | ||||
|     implementation("com.bluelinelabs:conductor-support:2.1.5") { | ||||
|         exclude group: "com.android.support" | ||||
|     } | ||||
|     implementation 'com.github.inorichi:conductor-support-preference:78e2344' | ||||
|     implementation 'com.github.inorichi:conductor-support-preference:a32c357' | ||||
|  | ||||
|     // RxBindings | ||||
|     final rxbindings_version = '1.0.1' | ||||
|     implementation "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version" | ||||
|     implementation "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version" | ||||
|     implementation "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version" | ||||
|     implementation "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version" | ||||
|     // FlowBinding | ||||
|     final flowbinding_version = '0.11.1' | ||||
|     implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version" | ||||
|     implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version" | ||||
|     implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version" | ||||
|     implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version" | ||||
|     implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version" | ||||
|  | ||||
|     // Tests | ||||
|     testImplementation 'junit:junit:4.12' | ||||
|     testImplementation 'org.assertj:assertj-core:1.7.1' | ||||
|     testImplementation 'junit:junit:4.13' | ||||
|     testImplementation 'org.assertj:assertj-core:3.12.2' | ||||
|     testImplementation 'org.mockito:mockito-core:1.10.19' | ||||
|  | ||||
|     final robolectric_version = '3.1.4' | ||||
| @@ -260,20 +280,22 @@ dependencies { | ||||
|     testImplementation "org.robolectric:shadows-multidex:$robolectric_version" | ||||
|     testImplementation "org.robolectric:shadows-play-services:$robolectric_version" | ||||
|  | ||||
|     implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" | ||||
|     implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" | ||||
|     implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" | ||||
|  | ||||
|     final coroutines_version = '1.3.3' | ||||
|     final coroutines_version = '1.3.5' | ||||
|     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" | ||||
|     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" | ||||
|     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$coroutines_version" | ||||
|     implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$coroutines_version" | ||||
|  | ||||
|     implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0' | ||||
|  | ||||
|     // Text distance (EH) | ||||
|     implementation 'info.debatty:java-string-similarity:1.2.1' | ||||
|  | ||||
|     // Pin lock view (EH) | ||||
|     implementation 'com.andrognito.pinlockview:pinlockview:2.1.0' | ||||
|     implementation 'com.github.jawnnypoo:pinlockview:2.2.0' | ||||
|  | ||||
|     // Reprint (EH) | ||||
|     implementation 'com.github.ajalt.reprint:core:3.2.1@aar' | ||||
| @@ -283,10 +305,7 @@ dependencies { | ||||
|     implementation 'com.mattprecious.swirl:swirl:1.2.0' | ||||
|  | ||||
|     // RxJava 2 interop for Realm (EH) | ||||
|     implementation 'com.lvla.android:rxjava2-interop-kt:0.2.1' | ||||
|  | ||||
|     // Debug network interceptor (EH) | ||||
|     implementation "com.squareup.okhttp3:logging-interceptor:4.2.1" | ||||
|     implementation 'com.github.akarnokd:rxjava2-interop:0.13.7' | ||||
|  | ||||
|     // Firebase (EH) | ||||
|     implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' | ||||
| @@ -323,7 +342,7 @@ dependencies { | ||||
| } | ||||
|  | ||||
| buildscript { | ||||
|     ext.kotlin_version = '1.3.61' | ||||
|     ext.kotlin_version = '1.3.72' | ||||
|     repositories { | ||||
|         mavenCentral() | ||||
|     } | ||||
| @@ -341,10 +360,15 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all { | ||||
|     kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"] | ||||
| } | ||||
|  | ||||
| androidExtensions { | ||||
|     experimental = true | ||||
| // Duplicating Hebrew string assets due to some locale code issues on different devices | ||||
| task copyResources(type: Copy) { | ||||
|     from './src/main/res/values-he' | ||||
|     into './src/main/res/values-iw' | ||||
|     include '**/*' | ||||
| } | ||||
|  | ||||
| preBuild.dependsOn(ktlintFormat, copyResources) | ||||
|  | ||||
| if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) { | ||||
|     apply plugin: 'com.google.gms.google-services' | ||||
|     // Firebase (EH) | ||||
|   | ||||
							
								
								
									
										36
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										36
									
								
								app/proguard-rules.pro
									
									
									
									
										vendored
									
									
								
							| @@ -1,22 +1,15 @@ | ||||
| #-repackageclasses '' | ||||
| -dontobfuscate | ||||
|  | ||||
| # == Make debugging easier | ||||
| -renamesourcefileattribute SourceFile | ||||
| -keepattributes SourceFile,LineNumberTable | ||||
|  | ||||
| # === Keep app classes | ||||
| # Extensions may require methods unused in the core app | ||||
| -dontwarn eu.kanade.tachiyomi.** | ||||
| -keep class eu.kanade.tachiyomi.** { *; } | ||||
| -keep class eu.kanade.tachiyomi.** { public protected private *; } | ||||
|  | ||||
| # === Keep extension classes | ||||
| -keep class org.jsoup.** { *; } | ||||
| -keep class kotlin.** { *; } | ||||
| -keep class okhttp3.** { *; } | ||||
| -keep class com.google.gson.** { *; } | ||||
| -keep class com.github.salomonbrys.kotson.** { *; } | ||||
| -keep class com.squareup.duktape.** { *; } | ||||
| -keep class android.support.v7.preference.** { *; } | ||||
| -keep class uy.kohesive.injekt.** { *; } | ||||
|  | ||||
| # === Keep EH classes | ||||
| -keep class exh.** { *; } | ||||
| @@ -25,7 +18,20 @@ | ||||
| # === Keep RxAndroid, https://github.com/ReactiveX/RxAndroid/issues/350 | ||||
| -keep class rx.android.** { *; } | ||||
|  | ||||
| # === RxJava 1.3.8 | ||||
| # Design library | ||||
| -dontwarn com.google.android.material.** | ||||
| -keep class com.google.android.material.** { *; } | ||||
| -keep interface com.google.android.material.** { *; } | ||||
| -keep public class com.google.android.material.R$* { *; } | ||||
|  | ||||
| -keep class com.hippo.image.** { *; } | ||||
| -keep interface com.hippo.image.** { *; } | ||||
| -keepclassmembers class * extends nucleus.presenter.Presenter { | ||||
|     <init>(); | ||||
| } | ||||
|  | ||||
|  | ||||
| # RxJava 1.1.0 | ||||
| -dontwarn sun.misc.** | ||||
|  | ||||
| -keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* { | ||||
| @@ -130,6 +136,7 @@ | ||||
|  | ||||
| # === Humanize + Guava: https://github.com/google/guava/wiki/UsingProGuardWithGuava | ||||
| -dontwarn javax.lang.model.element.Modifier | ||||
| -keep class org.ocpsoft.prettytime.i18n.** | ||||
|  | ||||
| # Note: We intentionally don't add the flags we'd need to make Enums work. | ||||
| # That's because the Proguard configuration required to make it work on | ||||
| @@ -223,10 +230,3 @@ | ||||
| -keep class com.google.apphosting.api.ApiProxy { | ||||
|   static *** getCurrentEnvironment (...); | ||||
| } | ||||
|  | ||||
| # === Support library | ||||
| # From original config: http://stackoverflow.com/questions/29679177/cardview-shadow-not-appearing-in-lollipop-after-obfuscate-with-proguard/29698051 | ||||
| -keep class android.support.v7.widget.RoundRectDrawable { *; } | ||||
|  | ||||
| # Fix missing back button: https://stackoverflow.com/a/46207775/5054192 | ||||
| -keep class android.support.v7.graphics.** { *; } | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|  | ||||
|     <shortcut | ||||
|         android:enabled="true" | ||||
|         android:icon="@drawable/sc_book_48dp" | ||||
|         android:icon="@drawable/sc_collections_bookmark_48dp" | ||||
|         android:shortcutDisabledMessage="@string/app_not_available" | ||||
|         android:shortcutId="show_library" | ||||
|         android:shortcutLongLabel="@string/label_library" | ||||
| @@ -13,7 +13,7 @@ | ||||
|     </shortcut> | ||||
|     <shortcut | ||||
|         android:enabled="true" | ||||
|         android:icon="@drawable/sc_update_48dp" | ||||
|         android:icon="@drawable/sc_new_releases_48dp" | ||||
|         android:shortcutDisabledMessage="@string/app_not_available" | ||||
|         android:shortcutId="show_recently_updated" | ||||
|         android:shortcutLongLabel="@string/label_recent_updates" | ||||
| @@ -24,7 +24,7 @@ | ||||
|     </shortcut> | ||||
|     <shortcut | ||||
|         android:enabled="true" | ||||
|         android:icon="@drawable/sc_glasses_48dp" | ||||
|         android:icon="@drawable/sc_history_48dp" | ||||
|         android:shortcutDisabledMessage="@string/app_not_available" | ||||
|         android:shortcutId="show_recently_read" | ||||
|         android:shortcutLongLabel="@string/label_recent_manga" | ||||
| @@ -38,8 +38,8 @@ | ||||
|         android:icon="@drawable/sc_explore_48dp" | ||||
|         android:shortcutDisabledMessage="@string/app_not_available" | ||||
|         android:shortcutId="show_catalogues" | ||||
|         android:shortcutLongLabel="@string/label_catalogues" | ||||
|         android:shortcutShortLabel="@string/label_catalogues"> | ||||
|         android:shortcutLongLabel="@string/browse" | ||||
|         android:shortcutShortLabel="@string/browse"> | ||||
|         <intent | ||||
|             android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES" | ||||
|             android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> | ||||
|   | ||||
| @@ -9,6 +9,7 @@ | ||||
|     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> | ||||
|     <uses-permission android:name="android.permission.WAKE_LOCK" /> | ||||
|     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> | ||||
|     <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" /> | ||||
|     <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> | ||||
|     <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> | ||||
|     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | ||||
| @@ -27,44 +28,62 @@ | ||||
|         android:allowBackup="true" | ||||
|         android:fullBackupContent="@xml/backup_rules" | ||||
|         android:hardwareAccelerated="true" | ||||
|         android:hasFragileUserData="true" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:label="@string/app_name" | ||||
|         android:largeHeap="true" | ||||
|         android:requestLegacyExternalStorage="true" | ||||
|         android:roundIcon="@mipmap/ic_launcher_round" | ||||
|         android:usesCleartextTraffic="true" | ||||
|         android:networkSecurityConfig="@xml/network_security_config" | ||||
|         android:theme="@style/Theme.Tachiyomi"> | ||||
|         android:theme="@style/Theme.Tachiyomi.Light" | ||||
|         android:usesCleartextTraffic="true"> | ||||
|         <activity | ||||
|             android:name=".ui.main.MainActivity" | ||||
|             android:launchMode="singleTask"> | ||||
|             android:launchMode="singleTop" | ||||
|             android:theme="@style/Theme.Splash"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.MAIN" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.LAUNCHER" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEARCH" /> | ||||
|                 <action android:name="com.google.android.gms.actions.SEARCH_ACTION"/> | ||||
|                 <category android:name="android.intent.category.DEFAULT"/> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="eu.kanade.tachiyomi.SEARCH" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT"/> | ||||
|             </intent-filter> | ||||
|             <meta-data android:name="android.app.searchable" android:resource="@xml/searchable"/> | ||||
|             <!-- suppress AndroidDomInspection --> | ||||
|             <!--suppress AndroidDomInspection --> | ||||
|             <meta-data | ||||
|                 android:name="android.app.shortcuts" | ||||
|                 android:resource="@xml/shortcuts" /> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".ui.reader.ReaderActivity" /> | ||||
|             android:name=".ui.main.DeepLinkActivity" | ||||
|             android:launchMode="singleTask" | ||||
|             android:theme="@android:style/Theme.NoDisplay"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.SEARCH" /> | ||||
|                 <action android:name="com.google.android.gms.actions.SEARCH_ACTION" /> | ||||
|  | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|             </intent-filter> | ||||
|             <intent-filter> | ||||
|                 <action android:name="eu.kanade.tachiyomi.SEARCH" /> | ||||
|                 <category android:name="android.intent.category.DEFAULT" /> | ||||
|             </intent-filter> | ||||
|  | ||||
|             <meta-data | ||||
|                 android:name="android.app.searchable" | ||||
|                 android:resource="@xml/searchable" /> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".ui.reader.ReaderActivity" | ||||
|             android:launchMode="singleTask" /> | ||||
|         <activity | ||||
|             android:name=".ui.security.BiometricUnlockActivity" | ||||
|             android:theme="@style/Theme.Splash" /> | ||||
|         <activity | ||||
|             android:name=".ui.webview.WebViewActivity" | ||||
|             android:configChanges="uiMode|orientation|screenSize" /> | ||||
|         <activity | ||||
|             android:name=".widget.CustomLayoutPickerActivity" | ||||
|             android:label="@string/app_name" | ||||
|             android:theme="@style/FilePickerTheme" /> | ||||
|         <activity | ||||
|             android:name=".ui.setting.AnilistLoginActivity" | ||||
|             android:name=".ui.setting.track.AnilistLoginActivity" | ||||
|             android:label="Anilist"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
| @@ -78,7 +97,7 @@ | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".ui.setting.ShikimoriLoginActivity" | ||||
|             android:name=".ui.setting.track.ShikimoriLoginActivity" | ||||
|             android:label="Shikimori"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
| @@ -92,7 +111,7 @@ | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|         <activity | ||||
|             android:name=".ui.setting.BangumiLoginActivity" | ||||
|             android:name=".ui.setting.track.BangumiLoginActivity" | ||||
|             android:label="Bangumi"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.VIEW" /> | ||||
| @@ -110,6 +129,13 @@ | ||||
|             android:name=".extension.util.ExtensionInstallActivity" | ||||
|             android:theme="@android:style/Theme.Translucent.NoTitleBar" /> | ||||
|  | ||||
|         <activity | ||||
|             android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity" | ||||
|             android:theme="@style/Theme.MaterialComponents" /> | ||||
|         <activity | ||||
|             android:name="com.google.android.gms.oss.licenses.OssLicensesActivity" | ||||
|             android:theme="@style/Theme.MaterialComponents" /> | ||||
|  | ||||
|         <provider | ||||
|             android:name="androidx.core.content.FileProvider" | ||||
|             android:authorities="${applicationId}.provider" | ||||
| @@ -127,15 +153,19 @@ | ||||
|         <service | ||||
|             android:name=".data.library.LibraryUpdateService" | ||||
|             android:exported="false" /> | ||||
|  | ||||
|         <service | ||||
|             android:name=".data.download.DownloadService" | ||||
|             android:exported="false" /> | ||||
|  | ||||
|         <service | ||||
|             android:name=".data.updater.UpdaterService" | ||||
|             android:exported="false" /> | ||||
|  | ||||
|         <service | ||||
|             android:name=".data.backup.BackupCreateService" | ||||
|             android:exported="false" /> | ||||
|  | ||||
|         <service | ||||
|             android:name=".data.backup.BackupRestoreService" | ||||
|             android:exported="false" /> | ||||
| @@ -273,7 +303,6 @@ | ||||
|         <activity | ||||
|             android:name="exh.ui.captcha.BrowserActionActivity" | ||||
|             android:theme="@style/Theme.EHActivity" /> | ||||
|         <activity android:name="exh.ui.webview.WebViewActivity" /> | ||||
|     </application> | ||||
|  | ||||
| </manifest> | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi | ||||
| import android.app.Application | ||||
| import android.content.Context | ||||
| import android.content.res.Configuration | ||||
| <<<<<<< HEAD | ||||
| import android.graphics.Color | ||||
| import android.os.Build | ||||
| import android.os.Environment | ||||
| @@ -37,16 +38,28 @@ import io.realm.Realm | ||||
| import io.realm.RealmConfiguration | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.launch | ||||
| import androidx.lifecycle.Lifecycle | ||||
| import androidx.lifecycle.LifecycleObserver | ||||
| import androidx.lifecycle.OnLifecycleEvent | ||||
| import androidx.lifecycle.ProcessLifecycleOwner | ||||
| import androidx.multidex.MultiDex | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate | ||||
| import eu.kanade.tachiyomi.util.system.LocaleHelper | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.InjektScope | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import uy.kohesive.injekt.registry.default.DefaultRegistrar | ||||
| import java.io.File | ||||
| import java.security.NoSuchAlgorithmException | ||||
| import javax.net.ssl.SSLContext | ||||
| import kotlin.concurrent.thread | ||||
|  | ||||
| open class App : Application() { | ||||
|  | ||||
| open class App : Application(), LifecycleObserver { | ||||
|  | ||||
|     override fun onCreate() { | ||||
|         super.onCreate() | ||||
|         if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) | ||||
| @@ -57,7 +70,6 @@ open class App : Application() { | ||||
|         Injekt = InjektScope(DefaultRegistrar()) | ||||
|         Injekt.importModule(AppModule(this)) | ||||
|  | ||||
|         setupJobManager() | ||||
|         setupNotificationChannels() | ||||
|         GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH) | ||||
|         Reprint.initialize(this) //Setup fingerprint (EH) | ||||
| @@ -66,6 +78,8 @@ open class App : Application() { | ||||
|         } | ||||
|  | ||||
|         LocaleHelper.updateConfiguration(this, resources.configuration) | ||||
|  | ||||
|         ProcessLifecycleOwner.get().lifecycle.addObserver(this) | ||||
|     } | ||||
|  | ||||
|     override fun attachBaseContext(base: Context) { | ||||
| @@ -97,18 +111,12 @@ open class App : Application() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected open fun setupJobManager() { | ||||
|         try { | ||||
|             JobManager.create(this).addJobCreator { tag -> | ||||
|                 when (tag) { | ||||
|                     LibraryUpdateJob.TAG -> LibraryUpdateJob() | ||||
|                     UpdaterJob.TAG -> UpdaterJob() | ||||
|                     BackupCreatorJob.TAG -> BackupCreatorJob() | ||||
|                     else -> null | ||||
|                 } | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             Timber.w("Can't initialize job manager") | ||||
|     @OnLifecycleEvent(Lifecycle.Event.ON_STOP) | ||||
|     @Suppress("unused") | ||||
|     fun onAppBackgrounded() { | ||||
|         val preferences: PreferencesHelper by injectLazy() | ||||
|         if (preferences.lockAppAfter().get() >= 0) { | ||||
|             SecureActivityDelegate.locked = true | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -11,16 +11,17 @@ import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.extension.ExtensionManager | ||||
| import eu.kanade.tachiyomi.network.NetworkHelper | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import exh.eh.EHentaiUpdateHelper | ||||
| import io.noties.markwon.Markwon | ||||
| import rx.Observable | ||||
| import rx.schedulers.Schedulers | ||||
| import uy.kohesive.injekt.api.* | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.launch | ||||
| import uy.kohesive.injekt.api.InjektModule | ||||
| import uy.kohesive.injekt.api.InjektRegistrar | ||||
| import uy.kohesive.injekt.api.addSingleton | ||||
| import uy.kohesive.injekt.api.addSingletonFactory | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class AppModule(val app: Application) : InjektModule { | ||||
|  | ||||
|     override fun InjektRegistrar.registerInjectables() { | ||||
|  | ||||
|         addSingleton(app) | ||||
|  | ||||
|         addSingletonFactory { PreferencesHelper(app) } | ||||
| @@ -49,20 +50,14 @@ class AppModule(val app: Application) : InjektModule { | ||||
|  | ||||
|         // Asynchronously init expensive components for a faster cold start | ||||
|  | ||||
|         rxAsync { get<PreferencesHelper>() } | ||||
|         GlobalScope.launch { get<PreferencesHelper>() } | ||||
|  | ||||
|         rxAsync { get<NetworkHelper>() } | ||||
|         GlobalScope.launch { get<NetworkHelper>() } | ||||
|  | ||||
|         rxAsync { get<SourceManager>() } | ||||
|         GlobalScope.launch { get<SourceManager>() } | ||||
|  | ||||
|         rxAsync { get<DatabaseHelper>() } | ||||
|  | ||||
|         rxAsync { get<DownloadManager>() } | ||||
|         GlobalScope.launch { get<DatabaseHelper>() } | ||||
|  | ||||
|         GlobalScope.launch { get<DownloadManager>() } | ||||
|     } | ||||
|  | ||||
|     private fun rxAsync(block: () -> Unit) { | ||||
|         Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe() | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,9 +1,11 @@ | ||||
| package eu.kanade.tachiyomi | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.backup.BackupCreatorJob | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateJob | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.preference.getOrDefault | ||||
| import eu.kanade.tachiyomi.data.updater.UpdaterJob | ||||
| import eu.kanade.tachiyomi.extension.ExtensionUpdateJob | ||||
| import eu.kanade.tachiyomi.ui.library.LibrarySort | ||||
| import java.io.File | ||||
|  | ||||
| object Migrations { | ||||
| @@ -18,18 +20,33 @@ object Migrations { | ||||
|      */ | ||||
|     fun upgrade(preferences: PreferencesHelper): Boolean { | ||||
|         val context = preferences.context | ||||
|         val oldVersion = preferences.lastVersionCode().getOrDefault() | ||||
|         val oldVersion = preferences.lastVersionCode().get() | ||||
|  | ||||
|         // Cancel app updater job for debug builds that don't include it | ||||
|         if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) { | ||||
|             UpdaterJob.cancelTask(context) | ||||
|         } | ||||
|  | ||||
|         if (oldVersion < BuildConfig.VERSION_CODE) { | ||||
|             preferences.lastVersionCode().set(BuildConfig.VERSION_CODE) | ||||
|  | ||||
|             if (oldVersion == 0) return false | ||||
|             // Fresh install | ||||
|             if (oldVersion == 0) { | ||||
|                 // Set up default background tasks | ||||
|                 if (BuildConfig.INCLUDE_UPDATER) { | ||||
|                     UpdaterJob.setupTask(context) | ||||
|                 } | ||||
|                 ExtensionUpdateJob.setupTask(context) | ||||
|                 LibraryUpdateJob.setupTask(context) | ||||
|                 return false | ||||
|             } | ||||
|  | ||||
|             if (oldVersion < 14) { | ||||
|                 // Restore jobs after upgrading to evernote's job scheduler. | ||||
|                 if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) { | ||||
|                     UpdaterJob.setupTask() | ||||
|                 // Restore jobs after upgrading to Evernote's job scheduler. | ||||
|                 if (BuildConfig.INCLUDE_UPDATER) { | ||||
|                     UpdaterJob.setupTask(context) | ||||
|                 } | ||||
|                 LibraryUpdateJob.setupTask() | ||||
|                 LibraryUpdateJob.setupTask(context) | ||||
|             } | ||||
|             if (oldVersion < 15) { | ||||
|                 // Delete internal chapter cache dir. | ||||
| @@ -41,7 +58,7 @@ object Migrations { | ||||
|                 if (oldDir.exists()) { | ||||
|                     val destDir = context.getExternalFilesDir("covers") | ||||
|                     if (destDir != null) { | ||||
|                         oldDir.listFiles().forEach { | ||||
|                         oldDir.listFiles()?.forEach { | ||||
|                             it.renameTo(File(destDir, it.name)) | ||||
|                         } | ||||
|                     } | ||||
| @@ -57,12 +74,25 @@ object Migrations { | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             if (oldVersion < 43) { | ||||
|                 // Restore jobs after migrating from Evernote's job scheduler to WorkManager. | ||||
|                 if (BuildConfig.INCLUDE_UPDATER) { | ||||
|                     UpdaterJob.setupTask(context) | ||||
|                 } | ||||
|                 LibraryUpdateJob.setupTask(context) | ||||
|                 BackupCreatorJob.setupTask(context) | ||||
|  | ||||
|             // ===========[ ALL MIGRATIONS ABOVE HERE HAVE BEEN ALREADY REWRITTEN ]=========== | ||||
|  | ||||
|                 // New extension update check job | ||||
|                 ExtensionUpdateJob.setupTask(context) | ||||
|             } | ||||
|             if (oldVersion < 44) { | ||||
|                 // Reset sorting preference if using removed sort by source | ||||
|                 if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) { | ||||
|                     preferences.librarySortingMode().set(LibrarySort.ALPHA) | ||||
|                 } | ||||
|             } | ||||
|             return true | ||||
|         } | ||||
|         return false | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,23 +1,10 @@ | ||||
| package eu.kanade.tachiyomi.data.backup | ||||
| import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID | ||||
|  | ||||
| import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID | ||||
|  | ||||
| object BackupConst { | ||||
|  | ||||
|     const val INTENT_FILTER = "SettingsBackupFragment" | ||||
|     const val ACTION_BACKUP_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED_DIALOG" | ||||
|     const val ACTION_SET_PROGRESS_DIALOG = "$ID.$INTENT_FILTER.ACTION_SET_PROGRESS_DIALOG" | ||||
|     const val ACTION_ERROR_BACKUP_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_BACKUP_DIALOG" | ||||
|     const val ACTION_ERROR_RESTORE_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_RESTORE_DIALOG" | ||||
|     const val ACTION_RESTORE_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_RESTORE_COMPLETED_DIALOG" | ||||
|     const val ACTION = "$ID.$INTENT_FILTER.ACTION" | ||||
|     const val EXTRA_PROGRESS = "$ID.$INTENT_FILTER.EXTRA_PROGRESS" | ||||
|     const val EXTRA_AMOUNT = "$ID.$INTENT_FILTER.EXTRA_AMOUNT" | ||||
|     const val EXTRA_ERRORS = "$ID.$INTENT_FILTER.EXTRA_ERRORS" | ||||
|     const val EXTRA_CONTENT = "$ID.$INTENT_FILTER.EXTRA_CONTENT" | ||||
|     const val EXTRA_ERROR_MESSAGE = "$ID.$INTENT_FILTER.EXTRA_ERROR_MESSAGE" | ||||
|     const val EXTRA_URI = "$ID.$INTENT_FILTER.EXTRA_URI" | ||||
|     const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME" | ||||
|     const val EXTRA_ERROR_FILE_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH" | ||||
|     const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE" | ||||
| } | ||||
|     private const val NAME = "BackupRestoreServices" | ||||
|     const val EXTRA_URI = "$ID.$NAME.EXTRA_URI" | ||||
|     const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS" | ||||
| } | ||||
|   | ||||
| @@ -1,25 +1,22 @@ | ||||
| package eu.kanade.tachiyomi.data.backup | ||||
|  | ||||
| import android.app.IntentService | ||||
| import android.app.Service | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import com.google.gson.JsonArray | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID | ||||
| import android.os.Build | ||||
| import android.os.IBinder | ||||
| import android.os.PowerManager | ||||
| import com.hippo.unifile.UniFile | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.util.system.isServiceRunning | ||||
|  | ||||
| /** | ||||
|  * [IntentService] used to backup [Manga] information to [JsonArray] | ||||
|  * Service for backing up library information to a JSON file. | ||||
|  */ | ||||
| class BackupCreateService : IntentService(NAME) { | ||||
| class BackupCreateService : Service() { | ||||
|  | ||||
|     companion object { | ||||
|         // Name of class | ||||
|         private const val NAME = "BackupCreateService" | ||||
|  | ||||
|         // Options for backup | ||||
|         private const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS" | ||||
|  | ||||
|         // Filter options | ||||
|         internal const val BACKUP_CATEGORY = 0x1 | ||||
|         internal const val BACKUP_CATEGORY_MASK = 0x1 | ||||
| @@ -31,6 +28,15 @@ class BackupCreateService : IntentService(NAME) { | ||||
|         internal const val BACKUP_TRACK_MASK = 0x8 | ||||
|         internal const val BACKUP_ALL = 0xF | ||||
|  | ||||
|         /** | ||||
|          * Returns the status of the service. | ||||
|          * | ||||
|          * @param context the application context. | ||||
|          * @return true if the service is running, false otherwise. | ||||
|          */ | ||||
|         fun isRunning(context: Context): Boolean = | ||||
|             context.isServiceRunning(BackupCreateService::class.java) | ||||
|  | ||||
|         /** | ||||
|          * Make a backup from library | ||||
|          * | ||||
| @@ -38,26 +44,78 @@ class BackupCreateService : IntentService(NAME) { | ||||
|          * @param uri path of Uri | ||||
|          * @param flags determines what to backup | ||||
|          */ | ||||
|         fun makeBackup(context: Context, uri: Uri, flags: Int) { | ||||
|             val intent = Intent(context, BackupCreateService::class.java).apply { | ||||
|                 putExtra(BackupConst.EXTRA_URI, uri) | ||||
|                 putExtra(EXTRA_FLAGS, flags) | ||||
|         fun start(context: Context, uri: Uri, flags: Int) { | ||||
|             if (!isRunning(context)) { | ||||
|                 val intent = Intent(context, BackupCreateService::class.java).apply { | ||||
|                     putExtra(BackupConst.EXTRA_URI, uri) | ||||
|                     putExtra(BackupConst.EXTRA_FLAGS, flags) | ||||
|                 } | ||||
|                 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { | ||||
|                     context.startService(intent) | ||||
|                 } else { | ||||
|                     context.startForegroundService(intent) | ||||
|                 } | ||||
|             } | ||||
|             context.startService(intent) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Wake lock that will be held until the service is destroyed. | ||||
|      */ | ||||
|     private lateinit var wakeLock: PowerManager.WakeLock | ||||
|  | ||||
|     private lateinit var backupManager: BackupManager | ||||
|     private lateinit var notifier: BackupNotifier | ||||
|  | ||||
|     override fun onCreate() { | ||||
|         super.onCreate() | ||||
|         notifier = BackupNotifier(this) | ||||
|  | ||||
|         startForeground(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build()) | ||||
|  | ||||
|         wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( | ||||
|             PowerManager.PARTIAL_WAKE_LOCK, "${javaClass.name}:WakeLock" | ||||
|         ) | ||||
|         wakeLock.acquire() | ||||
|     } | ||||
|  | ||||
|     override fun stopService(name: Intent?): Boolean { | ||||
|         destroyJob() | ||||
|         return super.stopService(name) | ||||
|     } | ||||
|  | ||||
|     override fun onDestroy() { | ||||
|         destroyJob() | ||||
|         super.onDestroy() | ||||
|     } | ||||
|  | ||||
|     private fun destroyJob() { | ||||
|         if (wakeLock.isHeld) { | ||||
|             wakeLock.release() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * This method needs to be implemented, but it's not used/needed. | ||||
|      */ | ||||
|     override fun onBind(intent: Intent): IBinder? = null | ||||
|  | ||||
|     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | ||||
|         if (intent == null) return START_NOT_STICKY | ||||
|  | ||||
|         try { | ||||
|             val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) | ||||
|             val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0) | ||||
|             backupManager = BackupManager(this) | ||||
|  | ||||
|             val backupFileUri = Uri.parse(backupManager.createBackup(uri, backupFlags, false)) | ||||
|             val unifile = UniFile.fromUri(this, backupFileUri) | ||||
|             notifier.showBackupComplete(unifile) | ||||
|         } catch (e: Exception) { | ||||
|             notifier.showBackupError(e.message) | ||||
|         } | ||||
|  | ||||
|         stopSelf(startId) | ||||
|         return START_NOT_STICKY | ||||
|     } | ||||
|  | ||||
|     private val backupManager by lazy { BackupManager(this) } | ||||
|  | ||||
|     override fun onHandleIntent(intent: Intent?) { | ||||
|         if (intent == null) return | ||||
|  | ||||
|         // Get values | ||||
|         val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) | ||||
|         val flags = intent.getIntExtra(EXTRA_FLAGS, 0) | ||||
|         // Create backup | ||||
|         backupManager.createBackup(uri, flags, false) | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -1,42 +1,51 @@ | ||||
| package eu.kanade.tachiyomi.data.backup | ||||
|  | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| import com.evernote.android.job.Job | ||||
| import com.evernote.android.job.JobManager | ||||
| import com.evernote.android.job.JobRequest | ||||
| import androidx.work.ExistingPeriodicWorkPolicy | ||||
| import androidx.work.PeriodicWorkRequestBuilder | ||||
| import androidx.work.WorkManager | ||||
| import androidx.work.Worker | ||||
| import androidx.work.WorkerParameters | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.preference.getOrDefault | ||||
| import java.util.concurrent.TimeUnit | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class BackupCreatorJob : Job() { | ||||
| class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|     Worker(context, workerParams) { | ||||
|  | ||||
|     override fun onRunJob(params: Params): Result { | ||||
|     override fun doWork(): Result { | ||||
|         val preferences = Injekt.get<PreferencesHelper>() | ||||
|         val backupManager = BackupManager(context) | ||||
|         val uri = Uri.parse(preferences.backupsDirectory().getOrDefault()) | ||||
|         val uri = Uri.parse(preferences.backupsDirectory().get()) | ||||
|         val flags = BackupCreateService.BACKUP_ALL | ||||
|         backupManager.createBackup(uri, flags, true) | ||||
|         return Result.SUCCESS | ||||
|         return try { | ||||
|             backupManager.createBackup(uri, flags, true) | ||||
|             Result.success() | ||||
|         } catch (e: Exception) { | ||||
|             Result.failure() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         const val TAG = "BackupCreator" | ||||
|         private const val TAG = "BackupCreator" | ||||
|  | ||||
|         fun setupTask(prefInterval: Int? = null) { | ||||
|         fun setupTask(context: Context, prefInterval: Int? = null) { | ||||
|             val preferences = Injekt.get<PreferencesHelper>() | ||||
|             val interval = prefInterval ?: preferences.backupInterval().getOrDefault() | ||||
|             val interval = prefInterval ?: preferences.backupInterval().get() | ||||
|             if (interval > 0) { | ||||
|                 JobRequest.Builder(TAG) | ||||
|                         .setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000) | ||||
|                         .setUpdateCurrent(true) | ||||
|                         .build() | ||||
|                         .schedule() | ||||
|             } | ||||
|         } | ||||
|                 val request = PeriodicWorkRequestBuilder<BackupCreatorJob>( | ||||
|                     interval.toLong(), TimeUnit.HOURS, | ||||
|                     10, TimeUnit.MINUTES | ||||
|                 ) | ||||
|                     .addTag(TAG) | ||||
|                     .build() | ||||
|  | ||||
|         fun cancelTask() { | ||||
|             JobManager.instance().cancelAllForTag(TAG) | ||||
|                 WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request) | ||||
|             } else { | ||||
|                 WorkManager.getInstance(context).cancelAllWorkByTag(TAG) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,10 +1,16 @@ | ||||
| package eu.kanade.tachiyomi.data.backup | ||||
|  | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import com.github.salomonbrys.kotson.* | ||||
| import com.google.gson.* | ||||
| import com.github.salomonbrys.kotson.fromJson | ||||
| import com.github.salomonbrys.kotson.registerTypeAdapter | ||||
| import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter | ||||
| import com.github.salomonbrys.kotson.set | ||||
| import com.google.gson.Gson | ||||
| import com.google.gson.GsonBuilder | ||||
| import com.google.gson.JsonArray | ||||
| import com.google.gson.JsonElement | ||||
| import com.google.gson.JsonObject | ||||
| import com.hippo.unifile.UniFile | ||||
| import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY | ||||
| import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK | ||||
| @@ -18,42 +24,42 @@ import eu.kanade.tachiyomi.data.backup.models.Backup | ||||
| import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES | ||||
| import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS | ||||
| import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION | ||||
| import eu.kanade.tachiyomi.data.backup.models.Backup.EXTENSIONS | ||||
| import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY | ||||
| import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA | ||||
| import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK | ||||
| import eu.kanade.tachiyomi.data.backup.models.DHistory | ||||
| import eu.kanade.tachiyomi.data.backup.serializer.* | ||||
| import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter | ||||
| import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter | ||||
| import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter | ||||
| import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter | ||||
| import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter | ||||
| import eu.kanade.tachiyomi.data.database.DatabaseHelper | ||||
| import eu.kanade.tachiyomi.data.database.models.* | ||||
| import eu.kanade.tachiyomi.data.database.models.CategoryImpl | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.ChapterImpl | ||||
| import eu.kanade.tachiyomi.data.database.models.History | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.database.models.MangaCategory | ||||
| import eu.kanade.tachiyomi.data.database.models.MangaImpl | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.database.models.TrackImpl | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.preference.getOrDefault | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.source.online.all.EHentai | ||||
| import eu.kanade.tachiyomi.util.sendLocalBroadcast | ||||
| import eu.kanade.tachiyomi.util.syncChaptersWithSource | ||||
| import exh.eh.EHentaiThrottleManager | ||||
| import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource | ||||
| import kotlin.math.max | ||||
| import rx.Observable | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|  | ||||
|     /** | ||||
|      * Database. | ||||
|      */ | ||||
|     internal val databaseHelper: DatabaseHelper by injectLazy() | ||||
|  | ||||
|     /** | ||||
|      * Source manager. | ||||
|      */ | ||||
|     internal val sourceManager: SourceManager by injectLazy() | ||||
|  | ||||
|     /** | ||||
|      * Tracking manager | ||||
|      */ | ||||
|     internal val trackManager: TrackManager by injectLazy() | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
|  | ||||
|     /** | ||||
|      * Version of parser | ||||
| @@ -66,11 +72,6 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      */ | ||||
|     var parser: Gson = initParser() | ||||
|  | ||||
|     /** | ||||
|      * Preferences | ||||
|      */ | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
|  | ||||
|     /** | ||||
|      * Set version of parser | ||||
|      * | ||||
| @@ -83,7 +84,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|  | ||||
|     private fun initParser(): Gson = when (version) { | ||||
|         1 -> GsonBuilder().create() | ||||
|         2 -> GsonBuilder() | ||||
|         2 -> | ||||
|             GsonBuilder() | ||||
|                 .registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build()) | ||||
|                 .registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build()) | ||||
|                 .registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build()) | ||||
| @@ -99,7 +101,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      * @param uri path of Uri | ||||
|      * @param isJob backup called from job | ||||
|      */ | ||||
|     fun createBackup(uri: Uri, flags: Int, isJob: Boolean) { | ||||
|     fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? { | ||||
|         // Create root object | ||||
|         val root = JsonObject() | ||||
|  | ||||
| @@ -109,24 +111,38 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|         // Create category array | ||||
|         val categoryEntries = JsonArray() | ||||
|  | ||||
|         // Create extension ID/name mapping | ||||
|         val extensionEntries = JsonArray() | ||||
|  | ||||
|         // Add value's to root | ||||
|         root[Backup.VERSION] = Backup.CURRENT_VERSION | ||||
|         root[Backup.VERSION] = CURRENT_VERSION | ||||
|         root[Backup.MANGAS] = mangaEntries | ||||
|         root[CATEGORIES] = categoryEntries | ||||
|         root[EXTENSIONS] = extensionEntries | ||||
|  | ||||
|         databaseHelper.inTransaction { | ||||
|             // Get manga from database | ||||
|             val mangas = getFavoriteManga() | ||||
|  | ||||
|             val extensions: MutableSet<String> = mutableSetOf() | ||||
|  | ||||
|             // Backup library manga and its dependencies | ||||
|             mangas.forEach { manga -> | ||||
|                 mangaEntries.add(backupMangaObject(manga, flags)) | ||||
|  | ||||
|                 // Maintain set of extensions/sources used (excludes local source) | ||||
|                 if (manga.source != 0L && sourceManager.get(manga.source) != null) { | ||||
|                     extensions.add("${manga.source}:${sourceManager.get(manga.source)!!.name}") | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Backup categories | ||||
|             if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) { | ||||
|                 backupCategories(categoryEntries) | ||||
|             } | ||||
|  | ||||
|             // Backup extension ID/name mapping | ||||
|             backupExtensionInfo(extensionEntries, extensions) | ||||
|         } | ||||
|  | ||||
|         try { | ||||
| @@ -140,42 +156,38 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|                 val numberOfBackups = numberOfBackups() | ||||
|                 val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""") | ||||
|                 dir.listFiles { _, filename -> backupRegex.matches(filename) } | ||||
|                         .orEmpty() | ||||
|                         .sortedByDescending { it.name } | ||||
|                         .drop(numberOfBackups - 1) | ||||
|                         .forEach { it.delete() } | ||||
|                     .orEmpty() | ||||
|                     .sortedByDescending { it.name } | ||||
|                     .drop(numberOfBackups - 1) | ||||
|                     .forEach { it.delete() } | ||||
|  | ||||
|                 // Create new file to place backup | ||||
|                 val newFile = dir.createFile(Backup.getDefaultFilename()) | ||||
|                         ?: throw Exception("Couldn't create backup file") | ||||
|                     ?: throw Exception("Couldn't create backup file") | ||||
|  | ||||
|                 newFile.openOutputStream().bufferedWriter().use { | ||||
|                     parser.toJson(root, it) | ||||
|                 } | ||||
|  | ||||
|                 return newFile.uri.toString() | ||||
|             } else { | ||||
|                 val file = UniFile.fromUri(context, uri) | ||||
|                         ?: throw Exception("Couldn't create backup file") | ||||
|                     ?: throw Exception("Couldn't create backup file") | ||||
|                 file.openOutputStream().bufferedWriter().use { | ||||
|                     parser.toJson(root, it) | ||||
|                 } | ||||
|  | ||||
|                 // Show completed dialog | ||||
|                 val intent = Intent(BackupConst.INTENT_FILTER).apply { | ||||
|                     putExtra(BackupConst.ACTION, BackupConst.ACTION_BACKUP_COMPLETED_DIALOG) | ||||
|                     putExtra(BackupConst.EXTRA_URI, file.uri.toString()) | ||||
|                 } | ||||
|                 context.sendLocalBroadcast(intent) | ||||
|                 return file.uri.toString() | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             Timber.e(e) | ||||
|             if (!isJob) { | ||||
|                 // Show error dialog | ||||
|                 val intent = Intent(BackupConst.INTENT_FILTER).apply { | ||||
|                     putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_BACKUP_DIALOG) | ||||
|                     putExtra(BackupConst.EXTRA_ERROR_MESSAGE, e.message) | ||||
|                 } | ||||
|                 context.sendLocalBroadcast(intent) | ||||
|             } | ||||
|             throw e | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) { | ||||
|         extensions.sorted().forEach { | ||||
|             root.add(it) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -206,7 +218,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|         if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) { | ||||
|             // Backup all the chapters | ||||
|             val chapters = databaseHelper.getChapters(manga).executeAsBlocking() | ||||
|             if (!chapters.isEmpty()) { | ||||
|             if (chapters.isNotEmpty()) { | ||||
|                 val chaptersJson = parser.toJsonTree(chapters) | ||||
|                 if (chaptersJson.asJsonArray.size() > 0) { | ||||
|                     entry[CHAPTERS] = chaptersJson | ||||
| @@ -218,7 +230,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|         if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { | ||||
|             // Backup categories for this manga | ||||
|             val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking() | ||||
|             if (!categoriesForManga.isEmpty()) { | ||||
|             if (categoriesForManga.isNotEmpty()) { | ||||
|                 val categoriesNames = categoriesForManga.map { it.name } | ||||
|                 entry[CATEGORIES] = parser.toJsonTree(categoriesNames) | ||||
|             } | ||||
| @@ -227,7 +239,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|         // Check if user wants track information in backup | ||||
|         if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) { | ||||
|             val tracks = databaseHelper.getTracks(manga).executeAsBlocking() | ||||
|             if (!tracks.isEmpty()) { | ||||
|             if (tracks.isNotEmpty()) { | ||||
|                 entry[TRACK] = parser.toJsonTree(tracks) | ||||
|             } | ||||
|         } | ||||
| @@ -235,7 +247,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|         // Check if user wants history information in backup | ||||
|         if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) { | ||||
|             val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking() | ||||
|             if (!historyForManga.isEmpty()) { | ||||
|             if (historyForManga.isNotEmpty()) { | ||||
|                 val historyData = historyForManga.mapNotNull { history -> | ||||
|                     val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url | ||||
|                     url?.let { DHistory(url, history.last_read) } | ||||
| @@ -266,13 +278,13 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      */ | ||||
|     fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> { | ||||
|         return source.fetchMangaDetails(manga) | ||||
|                 .map { networkManga -> | ||||
|                     manga.copyFrom(networkManga) | ||||
|                     manga.favorite = true | ||||
|                     manga.initialized = true | ||||
|                     manga.id = insertManga(manga) | ||||
|                     manga | ||||
|                 } | ||||
|             .map { networkManga -> | ||||
|                 manga.copyFrom(networkManga) | ||||
|                 manga.favorite = true | ||||
|                 manga.initialized = true | ||||
|                 manga.id = insertManga(manga) | ||||
|                 manga | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -282,18 +294,18 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      * @param manga manga that needs updating | ||||
|      * @return [Observable] that contains manga | ||||
|      */ | ||||
|     fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Observable<Pair<List<Chapter>, List<Chapter>>> { | ||||
|     fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> { | ||||
|         return (if(source is EHentai) { | ||||
|             source.fetchChapterList(manga, throttleManager::throttle) | ||||
|         } else { | ||||
|             source.fetchChapterList(manga) | ||||
|         }).map { syncChaptersWithSource(databaseHelper, it, manga, source) } | ||||
|                 .doOnNext { | ||||
|                     if (it.first.isNotEmpty()) { | ||||
|                         chapters.forEach { it.manga_id = manga.id } | ||||
|                         insertChapters(chapters) | ||||
|                     } | ||||
|             .map { syncChaptersWithSource(databaseHelper, it, manga, source) } | ||||
|             .doOnNext { pair -> | ||||
|                 if (pair.first.isNotEmpty()) { | ||||
|                     chapters.forEach { it.manga_id = manga.id } | ||||
|                     insertChapters(chapters) | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -349,7 +361,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|         } | ||||
|  | ||||
|         // Update database | ||||
|         if (!mangaCategoriesToUpdate.isEmpty()) { | ||||
|         if (mangaCategoriesToUpdate.isNotEmpty()) { | ||||
|             val mangaAsList = ArrayList<Manga>() | ||||
|             mangaAsList.add(manga) | ||||
|             databaseHelper.deleteOldMangasCategories(mangaAsList).executeAsBlocking() | ||||
| @@ -370,7 +382,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|             // Check if history already in database and update | ||||
|             if (dbHistory != null) { | ||||
|                 dbHistory.apply { | ||||
|                     last_read = Math.max(lastRead, dbHistory.last_read) | ||||
|                     last_read = max(lastRead, dbHistory.last_read) | ||||
|                 } | ||||
|                 historyToBeUpdated.add(dbHistory) | ||||
|             } else { | ||||
| @@ -413,7 +425,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|                         if (track.library_id != dbTrack.library_id) { | ||||
|                             dbTrack.library_id = track.library_id | ||||
|                         } | ||||
|                         dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read) | ||||
|                         dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read) | ||||
|                         isInDatabase = true | ||||
|                         trackToUpdate.add(dbTrack) | ||||
|                         break | ||||
| @@ -427,7 +439,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|             } | ||||
|         } | ||||
|         // Update database | ||||
|         if (!trackToUpdate.isEmpty()) { | ||||
|         if (trackToUpdate.isNotEmpty()) { | ||||
|             databaseHelper.insertTracks(trackToUpdate).executeAsBlocking() | ||||
|         } | ||||
|     } | ||||
| @@ -443,8 +455,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|         val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking() | ||||
|  | ||||
|         // Return if fetch is needed | ||||
|         if (dbChapters.isEmpty() || dbChapters.size < chapters.size) | ||||
|         if (dbChapters.isEmpty() || dbChapters.size < chapters.size) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         for (chapter in chapters) { | ||||
|             val pos = dbChapters.indexOf(chapter) | ||||
| @@ -469,7 +482,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      * @return [Manga], null if not found | ||||
|      */ | ||||
|     internal fun getMangaFromDatabase(manga: Manga): Manga? = | ||||
|             databaseHelper.getManga(manga.url, manga.source).executeAsBlocking() | ||||
|         databaseHelper.getManga(manga.url, manga.source).executeAsBlocking() | ||||
|  | ||||
|     /** | ||||
|      * Returns list containing manga from library | ||||
| @@ -477,7 +490,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      * @return [Manga] from library | ||||
|      */ | ||||
|     internal fun getFavoriteManga(): List<Manga> = | ||||
|             databaseHelper.getFavoriteMangas().executeAsBlocking() | ||||
|         databaseHelper.getFavoriteMangas().executeAsBlocking() | ||||
|  | ||||
|     /** | ||||
|      * Inserts manga and returns id | ||||
| @@ -485,7 +498,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      * @return id of [Manga], null if not found | ||||
|      */ | ||||
|     internal fun insertManga(manga: Manga): Long? = | ||||
|             databaseHelper.insertManga(manga).executeAsBlocking().insertedId() | ||||
|         databaseHelper.insertManga(manga).executeAsBlocking().insertedId() | ||||
|  | ||||
|     /** | ||||
|      * Inserts list of chapters | ||||
| @@ -499,5 +512,5 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { | ||||
|      * | ||||
|      * @return number of backups selected by user | ||||
|      */ | ||||
|     fun numberOfBackups(): Int = preferences.numberOfBackups().getOrDefault() | ||||
|     fun numberOfBackups(): Int = preferences.numberOfBackups().get() | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,159 @@ | ||||
| package eu.kanade.tachiyomi.data.backup | ||||
|  | ||||
| import android.content.Context | ||||
| import android.graphics.BitmapFactory | ||||
| import androidx.core.app.NotificationCompat | ||||
| import com.hippo.unifile.UniFile | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.notification.NotificationReceiver | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.util.storage.getUriCompat | ||||
| import eu.kanade.tachiyomi.util.system.notificationBuilder | ||||
| import eu.kanade.tachiyomi.util.system.notificationManager | ||||
| import java.io.File | ||||
| import java.util.concurrent.TimeUnit | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| internal class BackupNotifier(private val context: Context) { | ||||
|  | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
|  | ||||
|     private val progressNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS) { | ||||
|         setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) | ||||
|         setSmallIcon(R.drawable.ic_tachi) | ||||
|         setAutoCancel(false) | ||||
|         setOngoing(true) | ||||
|     } | ||||
|  | ||||
|     private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) { | ||||
|         setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) | ||||
|         setSmallIcon(R.drawable.ic_tachi) | ||||
|         setAutoCancel(false) | ||||
|     } | ||||
|  | ||||
|     private fun NotificationCompat.Builder.show(id: Int) { | ||||
|         context.notificationManager.notify(id, build()) | ||||
|     } | ||||
|  | ||||
|     fun showBackupProgress(): NotificationCompat.Builder { | ||||
|         val builder = with(progressNotificationBuilder) { | ||||
|             setContentTitle(context.getString(R.string.creating_backup)) | ||||
|  | ||||
|             setProgress(0, 0, true) | ||||
|         } | ||||
|  | ||||
|         builder.show(Notifications.ID_BACKUP_PROGRESS) | ||||
|  | ||||
|         return builder | ||||
|     } | ||||
|  | ||||
|     fun showBackupError(error: String?) { | ||||
|         context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS) | ||||
|  | ||||
|         with(completeNotificationBuilder) { | ||||
|             setContentTitle(context.getString(R.string.creating_backup_error)) | ||||
|             setContentText(error) | ||||
|  | ||||
|             show(Notifications.ID_BACKUP_COMPLETE) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun showBackupComplete(unifile: UniFile) { | ||||
|         context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS) | ||||
|  | ||||
|         with(completeNotificationBuilder) { | ||||
|             setContentTitle(context.getString(R.string.backup_created)) | ||||
|  | ||||
|             if (unifile.filePath != null) { | ||||
|                 setContentText(unifile.filePath) | ||||
|             } | ||||
|  | ||||
|             // Clear old actions if they exist | ||||
|             if (mActions.isNotEmpty()) { | ||||
|                 mActions.clear() | ||||
|             } | ||||
|  | ||||
|             addAction( | ||||
|                 R.drawable.ic_share_24dp, | ||||
|                 context.getString(R.string.action_share), | ||||
|                 NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE) | ||||
|             ) | ||||
|  | ||||
|             show(Notifications.ID_BACKUP_COMPLETE) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun showRestoreProgress(content: String = "", progress: Int = 0, maxAmount: Int = 100): NotificationCompat.Builder { | ||||
|         val builder = with(progressNotificationBuilder) { | ||||
|             setContentTitle(context.getString(R.string.restoring_backup)) | ||||
|  | ||||
|             if (!preferences.hideNotificationContent()) { | ||||
|                 setContentText(content) | ||||
|             } | ||||
|  | ||||
|             setProgress(maxAmount, progress, false) | ||||
|  | ||||
|             // Clear old actions if they exist | ||||
|             if (mActions.isNotEmpty()) { | ||||
|                 mActions.clear() | ||||
|             } | ||||
|  | ||||
|             addAction( | ||||
|                 R.drawable.ic_close_24dp, | ||||
|                 context.getString(R.string.action_stop), | ||||
|                 NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS) | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         builder.show(Notifications.ID_RESTORE_PROGRESS) | ||||
|  | ||||
|         return builder | ||||
|     } | ||||
|  | ||||
|     fun showRestoreError(error: String?) { | ||||
|         context.notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS) | ||||
|  | ||||
|         with(completeNotificationBuilder) { | ||||
|             setContentTitle(context.getString(R.string.restoring_backup_error)) | ||||
|             setContentText(error) | ||||
|  | ||||
|             show(Notifications.ID_RESTORE_COMPLETE) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun showRestoreComplete(time: Long, errorCount: Int, path: String?, file: String?) { | ||||
|         context.notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS) | ||||
|  | ||||
|         val timeString = context.getString( | ||||
|             R.string.restore_duration, | ||||
|             TimeUnit.MILLISECONDS.toMinutes(time), | ||||
|             TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds( | ||||
|                 TimeUnit.MILLISECONDS.toMinutes(time) | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         with(completeNotificationBuilder) { | ||||
|             setContentTitle(context.getString(R.string.restore_completed)) | ||||
|             setContentText(context.getString(R.string.restore_completed_content, timeString, errorCount)) | ||||
|  | ||||
|             // Clear old actions if they exist | ||||
|             if (mActions.isNotEmpty()) { | ||||
|                 mActions.clear() | ||||
|             } | ||||
|  | ||||
|             if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) { | ||||
|                 val destFile = File(path, file) | ||||
|                 val uri = destFile.getUriCompat(context) | ||||
|  | ||||
|                 addAction( | ||||
|                     R.drawable.nnf_ic_file_folder, | ||||
|                     context.getString(R.string.action_open_log), | ||||
|                     NotificationReceiver.openErrorLogPendingActivity(context, uri) | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|             show(Notifications.ID_RESTORE_COMPLETE) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -10,6 +10,8 @@ import android.os.PowerManager | ||||
| import com.elvishew.xlog.XLog | ||||
| import com.github.salomonbrys.kotson.fromJson | ||||
| import com.google.gson.JsonArray | ||||
| import com.google.gson.JsonElement | ||||
| import com.google.gson.JsonObject | ||||
| import com.google.gson.JsonParser | ||||
| import com.google.gson.stream.JsonReader | ||||
| import eu.kanade.tachiyomi.R | ||||
| @@ -22,12 +24,16 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK | ||||
| import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION | ||||
| import eu.kanade.tachiyomi.data.backup.models.DHistory | ||||
| import eu.kanade.tachiyomi.data.database.DatabaseHelper | ||||
| import eu.kanade.tachiyomi.data.database.models.* | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.ChapterImpl | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.database.models.MangaImpl | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.database.models.TrackImpl | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.util.chop | ||||
| import eu.kanade.tachiyomi.util.isServiceRunning | ||||
| import eu.kanade.tachiyomi.util.sendLocalBroadcast | ||||
| import eu.kanade.tachiyomi.util.system.isServiceRunning | ||||
| import exh.BackupEntry | ||||
| import exh.EH_SOURCE_ID | ||||
| import exh.EXHMigrations | ||||
| @@ -42,11 +48,16 @@ import java.io.File | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Date | ||||
| import java.util.Locale | ||||
| import java.util.concurrent.ExecutorService | ||||
| import java.util.concurrent.Executors | ||||
| import kotlinx.coroutines.CoroutineExceptionHandler | ||||
| import kotlinx.coroutines.GlobalScope | ||||
| import kotlinx.coroutines.Job | ||||
| import kotlinx.coroutines.launch | ||||
| import rx.Observable | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| /** | ||||
|  * Restores backup from json file | ||||
|  * Restores backup from a JSON file. | ||||
|  */ | ||||
| class BackupRestoreService : Service() { | ||||
|  | ||||
| @@ -58,8 +69,8 @@ class BackupRestoreService : Service() { | ||||
|          * @param context the application context. | ||||
|          * @return true if the service is running, false otherwise. | ||||
|          */ | ||||
|         private fun isRunning(context: Context): Boolean = | ||||
|                 context.isServiceRunning(BackupRestoreService::class.java) | ||||
|         fun isRunning(context: Context): Boolean = | ||||
|             context.isServiceRunning(BackupRestoreService::class.java) | ||||
|  | ||||
|         /** | ||||
|          * Starts a service to restore a backup from Json | ||||
| @@ -72,7 +83,11 @@ class BackupRestoreService : Service() { | ||||
|                 val intent = Intent(context, BackupRestoreService::class.java).apply { | ||||
|                     putExtra(BackupConst.EXTRA_URI, uri) | ||||
|                 } | ||||
|                 context.startService(intent) | ||||
|                 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { | ||||
|                     context.startService(intent) | ||||
|                 } else { | ||||
|                     context.startForegroundService(intent) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -83,6 +98,8 @@ class BackupRestoreService : Service() { | ||||
|          */ | ||||
|         fun stop(context: Context) { | ||||
|             context.stopService(Intent(context, BackupRestoreService::class.java)) | ||||
|  | ||||
|             BackupNotifier(context).showRestoreError(context.getString(R.string.restoring_backup_canceled)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -91,10 +108,7 @@ class BackupRestoreService : Service() { | ||||
|      */ | ||||
|     private lateinit var wakeLock: PowerManager.WakeLock | ||||
|  | ||||
|     /** | ||||
|      * Subscription where the update is done. | ||||
|      */ | ||||
|     private var subscription: Subscription? = null | ||||
|     private var job: Job? = null | ||||
|  | ||||
|     /** | ||||
|      * The progress of a backup restore | ||||
| @@ -111,20 +125,12 @@ class BackupRestoreService : Service() { | ||||
|      */ | ||||
|     private val errors = mutableListOf<Pair<Date, String>>() | ||||
|  | ||||
|     /** | ||||
|      * Backup manager | ||||
|      */ | ||||
|     private lateinit var backupManager: BackupManager | ||||
|     private lateinit var notifier: BackupNotifier | ||||
|  | ||||
|     /** | ||||
|      * Database | ||||
|      */ | ||||
|     private val db: DatabaseHelper by injectLazy() | ||||
|  | ||||
|     /** | ||||
|      * Tracking manager | ||||
|      */ | ||||
|     internal val trackManager: TrackManager by injectLazy() | ||||
|     private val trackManager: TrackManager by injectLazy() | ||||
|  | ||||
|  | ||||
|     private lateinit var executor: ExecutorService | ||||
| @@ -136,23 +142,31 @@ class BackupRestoreService : Service() { | ||||
|      */ | ||||
|     override fun onCreate() { | ||||
|         super.onCreate() | ||||
|         notifier = BackupNotifier(this) | ||||
|  | ||||
|         startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build()) | ||||
|  | ||||
|         wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( | ||||
|                 PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock") | ||||
|             PowerManager.PARTIAL_WAKE_LOCK, "${javaClass.name}:WakeLock" | ||||
|         ) | ||||
|         wakeLock.acquire() | ||||
|         executor = Executors.newSingleThreadExecutor() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Method called when the service is destroyed. It destroys the running subscription and | ||||
|      * releases the wake lock. | ||||
|      */ | ||||
|     override fun stopService(name: Intent?): Boolean { | ||||
|         destroyJob() | ||||
|         return super.stopService(name) | ||||
|     } | ||||
|  | ||||
|     override fun onDestroy() { | ||||
|         subscription?.unsubscribe() | ||||
|         executor.shutdown() // must be called after unsubscribe | ||||
|         destroyJob() | ||||
|         super.onDestroy() | ||||
|     } | ||||
|  | ||||
|     private fun destroyJob() { | ||||
|         job?.cancel() | ||||
|         if (wakeLock.isHeld) { | ||||
|             wakeLock.release() | ||||
|         } | ||||
|         super.onDestroy() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -169,157 +183,109 @@ class BackupRestoreService : Service() { | ||||
|      * @return the start value of the command. | ||||
|      */ | ||||
|     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | ||||
|         if (intent == null) return Service.START_NOT_STICKY | ||||
|         val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY | ||||
|  | ||||
|         val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) | ||||
|         // Cancel any previous job if needed. | ||||
|         job?.cancel() | ||||
|         val handler = CoroutineExceptionHandler { _, exception -> | ||||
|             Timber.e(exception) | ||||
|             writeErrorLog() | ||||
|  | ||||
|         throttleManager.resetThrottle() | ||||
|             notifier.showRestoreError(exception.message) | ||||
|  | ||||
|         // Unsubscribe from any previous subscription if needed. | ||||
|         subscription?.unsubscribe() | ||||
|             stopSelf(startId) | ||||
|         } | ||||
|         job = GlobalScope.launch(handler) { | ||||
|             restoreBackup(uri) | ||||
|         } | ||||
|         job?.invokeOnCompletion { | ||||
|             stopSelf(startId) | ||||
|         } | ||||
|  | ||||
|         subscription = Observable.using( | ||||
|                 { | ||||
|                     // Pause auto-gallery-update during restore | ||||
|                     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|                         EHentaiUpdateWorker.cancelBackground(this) | ||||
|                     } | ||||
|                     db.lowLevel().beginTransaction() | ||||
|                 }, | ||||
|                 { getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } }, | ||||
|                 { | ||||
|                     // Resume auto-gallery-update | ||||
|                     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { | ||||
|                         EHentaiUpdateWorker.scheduleBackground(this) | ||||
|                     } | ||||
|                     executor.execute { db.lowLevel().endTransaction() } | ||||
|                 }) | ||||
|                 .doAfterTerminate { stopSelf(startId) } | ||||
|                 .subscribeOn(Schedulers.from(executor)) | ||||
|                 .subscribe() | ||||
|  | ||||
|         return Service.START_NOT_STICKY | ||||
|         return START_NOT_STICKY | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns an [Observable] containing restore process. | ||||
|      * Restores data from backup file. | ||||
|      * | ||||
|      * @param uri restore file | ||||
|      * @return [Observable<Manga>] | ||||
|      * @param uri backup file to restore | ||||
|      */ | ||||
|     private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> { | ||||
|     private fun restoreBackup(uri: Uri) { | ||||
|         val startTime = System.currentTimeMillis() | ||||
|  | ||||
|         return Observable.just(Unit) | ||||
|                 .map { | ||||
|                     val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader()) | ||||
|                     val json = JsonParser().parse(reader).asJsonObject | ||||
|         val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader()) | ||||
|         val json = JsonParser.parseReader(reader).asJsonObject | ||||
|  | ||||
|                     // Get parser version | ||||
|                     val version = json.get(VERSION)?.asInt ?: 1 | ||||
|         // Get parser version | ||||
|         val version = json.get(VERSION)?.asInt ?: 1 | ||||
|  | ||||
|                     // Initialize manager | ||||
|                     backupManager = BackupManager(this, version) | ||||
|         // Initialize manager | ||||
|         backupManager = BackupManager(this, version) | ||||
|  | ||||
|                     val mangasJson = json.get(MANGAS).asJsonArray | ||||
|         val mangasJson = json.get(MANGAS).asJsonArray | ||||
|  | ||||
|                     restoreAmount = mangasJson.size() + 1 // +1 for categories | ||||
|                     restoreProgress = 0 | ||||
|                     errors.clear() | ||||
|         restoreAmount = mangasJson.size() + 1 // +1 for categories | ||||
|         restoreProgress = 0 | ||||
|         errors.clear() | ||||
|  | ||||
|                     // Restore categories | ||||
|                     json.get(CATEGORIES)?.let { | ||||
|                         backupManager.restoreCategories(it.asJsonArray) | ||||
|                         restoreProgress += 1 | ||||
|                         showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size) | ||||
|                     } | ||||
|         // Restore categories | ||||
|         restoreCategories(json.get(CATEGORIES)) | ||||
|  | ||||
|                     mangasJson | ||||
|                 } | ||||
|                 .flatMap { Observable.from(it) } | ||||
|                 .concatMap { | ||||
|                     val obj = it.asJsonObject | ||||
|                     val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA)) | ||||
|                     val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(obj.get(CHAPTERS) ?: JsonArray()) | ||||
|                     val categories = backupManager.parser.fromJson<List<String>>(obj.get(CATEGORIES) ?: JsonArray()) | ||||
|                     val history = backupManager.parser.fromJson<List<DHistory>>(obj.get(HISTORY) ?: JsonArray()) | ||||
|                     val tracks = backupManager.parser.fromJson<List<TrackImpl>>(obj.get(TRACK) ?: JsonArray()) | ||||
|         // Restore individual manga | ||||
|         mangasJson.forEach { | ||||
|             restoreManga(it.asJsonObject) | ||||
|         } | ||||
|  | ||||
|                     // EXH --> | ||||
|                     val migrated = EXHMigrations.migrateBackupEntry( | ||||
|                             BackupEntry( | ||||
|                                     manga, | ||||
|                                     chapters, | ||||
|                                     categories, | ||||
|                                     history, | ||||
|                                     tracks | ||||
|                             ) | ||||
|                     ) | ||||
|         val endTime = System.currentTimeMillis() | ||||
|         val time = endTime - startTime | ||||
|  | ||||
|                     val observable = migrated.flatMap { (manga, chapters, categories, history, tracks) -> | ||||
|                         getMangaRestoreObservable(manga, chapters, categories, history, tracks) | ||||
|                     } | ||||
|                     // EXH <-- | ||||
|                     if (observable != null) { | ||||
|                         observable | ||||
|                     } else { | ||||
|                         errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}") | ||||
|                         restoreProgress += 1 | ||||
|                         val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15)) | ||||
|                         showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size, content) | ||||
|                         Observable.just(manga) | ||||
|                     } | ||||
|                 } | ||||
|                 .toList() | ||||
|                 .doOnNext { | ||||
|                     val endTime = System.currentTimeMillis() | ||||
|                     val time = endTime - startTime | ||||
|                     val logFile = writeErrorLog() | ||||
|                     val completeIntent = Intent(BackupConst.INTENT_FILTER).apply { | ||||
|                         putExtra(BackupConst.EXTRA_TIME, time) | ||||
|                         putExtra(BackupConst.EXTRA_ERRORS, errors.size) | ||||
|                         putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent) | ||||
|                         putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name) | ||||
|                         putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED_DIALOG) | ||||
|                     } | ||||
|                     sendLocalBroadcast(completeIntent) | ||||
|         val logFile = writeErrorLog() | ||||
|  | ||||
|                 } | ||||
|                 .doOnError { error -> | ||||
|                     // [EXH] | ||||
|                     XLog.w("> Failed to perform restore!", error) | ||||
|                     XLog.w("> (uri: %s)", uri) | ||||
|  | ||||
|                     writeErrorLog() | ||||
|                     val errorIntent = Intent(BackupConst.INTENT_FILTER).apply { | ||||
|                         putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG) | ||||
|                         putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message) | ||||
|                     } | ||||
|                     sendLocalBroadcast(errorIntent) | ||||
|                 } | ||||
|                 .onErrorReturn { emptyList() } | ||||
|         notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Write errors to error log | ||||
|      */ | ||||
|     private fun writeErrorLog(): File { | ||||
|         try { | ||||
|             if (errors.isNotEmpty()) { | ||||
|                 val destFile = File(externalCacheDir, "tachiyomi_restore.log") | ||||
|                 val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) | ||||
|     private fun restoreCategories(categoriesJson: JsonElement) { | ||||
|         db.inTransaction { | ||||
|             backupManager.restoreCategories(categoriesJson.asJsonArray) | ||||
|  | ||||
|                 destFile.bufferedWriter().use { out -> | ||||
|                     errors.forEach { (date, message) -> | ||||
|                         out.write("[${sdf.format(date)}] $message\n") | ||||
|                     } | ||||
|                 } | ||||
|                 return destFile | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             // Empty | ||||
|             restoreProgress += 1 | ||||
|             showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun restoreManga(mangaJson: JsonObject) { | ||||
|         db.inTransaction { | ||||
|             val manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA)) | ||||
|             val chapters = backupManager.parser.fromJson<List<ChapterImpl>>( | ||||
|                 mangaJson.get(CHAPTERS) | ||||
|                     ?: JsonArray() | ||||
|             ) | ||||
|             val categories = backupManager.parser.fromJson<List<String>>( | ||||
|                 mangaJson.get(CATEGORIES) | ||||
|                     ?: JsonArray() | ||||
|             ) | ||||
|             val history = backupManager.parser.fromJson<List<DHistory>>( | ||||
|                 mangaJson.get(HISTORY) | ||||
|                     ?: JsonArray() | ||||
|             ) | ||||
|             val tracks = backupManager.parser.fromJson<List<TrackImpl>>( | ||||
|                 mangaJson.get(TRACK) | ||||
|                     ?: JsonArray() | ||||
|             ) | ||||
|  | ||||
|             if (job?.isActive != true) { | ||||
|                 throw Exception(getString(R.string.restoring_backup_canceled)) | ||||
|             } | ||||
|  | ||||
|             try { | ||||
|                 restoreMangaData(manga, chapters, categories, history, tracks) | ||||
|             } catch (e: Exception) { | ||||
|                 errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}") | ||||
|             } | ||||
|  | ||||
|             restoreProgress += 1 | ||||
|             showRestoreProgress(restoreProgress, restoreAmount, manga.title) | ||||
|         } | ||||
|         return File("") | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -330,23 +296,26 @@ class BackupRestoreService : Service() { | ||||
|      * @param categories categories data from json | ||||
|      * @param history history data from json | ||||
|      * @param tracks tracking data from json | ||||
|      * @return [Observable] containing manga restore information | ||||
|      */ | ||||
|     private fun getMangaRestoreObservable(manga: Manga, chapters: List<Chapter>, | ||||
|                                           categories: List<String>, history: List<DHistory>, | ||||
|                                           tracks: List<Track>): Observable<Manga>? { | ||||
|     private fun restoreMangaData( | ||||
|         manga: Manga, | ||||
|         chapters: List<Chapter>, | ||||
|         categories: List<String>, | ||||
|         history: List<DHistory>, | ||||
|         tracks: List<Track> | ||||
|     ) { | ||||
|         // Get source | ||||
|         val source = backupManager.sourceManager.getOrStub(manga.source) | ||||
|         val dbManga = backupManager.getMangaFromDatabase(manga) | ||||
|  | ||||
|         return if (dbManga == null) { | ||||
|         if (dbManga == null) { | ||||
|             // Manga not in database | ||||
|             mangaFetchObservable(source, manga, chapters, categories, history, tracks) | ||||
|             restoreMangaFetch(source, manga, chapters, categories, history, tracks) | ||||
|         } else { // Manga in database | ||||
|             // Copy information from manga already in database | ||||
|             backupManager.restoreMangaNoFetch(manga, dbManga) | ||||
|             // Fetch rest of manga information | ||||
|             mangaNoFetchObservable(source, manga, chapters, categories, history, tracks) | ||||
|             restoreMangaNoFetch(source, manga, chapters, categories, history, tracks) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -357,70 +326,58 @@ class BackupRestoreService : Service() { | ||||
|      * @param chapters chapters of manga that needs updating | ||||
|      * @param categories categories that need updating | ||||
|      */ | ||||
|     private fun mangaFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, | ||||
|                                      categories: List<String>, history: List<DHistory>, | ||||
|                                      tracks: List<Track>): Observable<Manga> { | ||||
|         if(source.id == EH_SOURCE_ID || source.id == EXH_SOURCE_ID) | ||||
|             throttleManager.throttle() | ||||
|  | ||||
|         return backupManager.restoreMangaFetchObservable(source, manga) | ||||
|                 .onErrorReturn { | ||||
|                     // [EXH] | ||||
|                     XLog.w("> Failed to restore manga!", it) | ||||
|                     XLog.w("> (source.id: %s, source.name: %s, manga.id: %s, manga.url: %s)", | ||||
|                             source.id, | ||||
|                             source.name, | ||||
|                             manga.id, | ||||
|                             manga.url) | ||||
|  | ||||
|                     errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                     manga | ||||
|                 } | ||||
|                 .filter { it.id != null } | ||||
|                 .flatMap { | ||||
|                     chapterFetchObservable(source, it, chapters) | ||||
|                             // Convert to the manga that contains new chapters. | ||||
|                             .map { manga } | ||||
|                 } | ||||
|                 .doOnNext { | ||||
|                     restoreExtraForManga(it, categories, history, tracks) | ||||
|                 } | ||||
|                 .flatMap { | ||||
|                     trackingFetchObservable(it, tracks) | ||||
|                             // Convert to the manga that contains new chapters. | ||||
|                             .map { manga } | ||||
|                 } | ||||
|                 .doOnCompleted { | ||||
|                     restoreProgress += 1 | ||||
|                     showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size) | ||||
|                 } | ||||
|     private fun restoreMangaFetch( | ||||
|         source: Source, | ||||
|         manga: Manga, | ||||
|         chapters: List<Chapter>, | ||||
|         categories: List<String>, | ||||
|         history: List<DHistory>, | ||||
|         tracks: List<Track> | ||||
|     ) { | ||||
|         backupManager.restoreMangaFetchObservable(source, manga) | ||||
|             .onErrorReturn { | ||||
|                 errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                 manga | ||||
|             } | ||||
|             .filter { it.id != null } | ||||
|             .flatMap { | ||||
|                 chapterFetchObservable(source, it, chapters) | ||||
|                     // Convert to the manga that contains new chapters. | ||||
|                     .map { manga } | ||||
|             } | ||||
|             .doOnNext { | ||||
|                 restoreExtraForManga(it, categories, history, tracks) | ||||
|             } | ||||
|             .flatMap { | ||||
|                 trackingFetchObservable(it, tracks) | ||||
|             } | ||||
|             .subscribe() | ||||
|     } | ||||
|  | ||||
|     private fun mangaNoFetchObservable(source: Source, backupManga: Manga, chapters: List<Chapter>, | ||||
|                                        categories: List<String>, history: List<DHistory>, | ||||
|                                        tracks: List<Track>): Observable<Manga> { | ||||
|  | ||||
|         return Observable.just(backupManga) | ||||
|                 .flatMap { manga -> | ||||
|                     if (!backupManager.restoreChaptersForManga(manga, chapters)) { | ||||
|                         chapterFetchObservable(source, manga, chapters) | ||||
|                                 .map { manga } | ||||
|                     } else { | ||||
|                         Observable.just(manga) | ||||
|                     } | ||||
|                 } | ||||
|                 .doOnNext { | ||||
|                     restoreExtraForManga(it, categories, history, tracks) | ||||
|                 } | ||||
|                 .flatMap { manga -> | ||||
|                     trackingFetchObservable(manga, tracks) | ||||
|                             // Convert to the manga that contains new chapters. | ||||
|                             .map { manga } | ||||
|                 } | ||||
|                 .doOnCompleted { | ||||
|                     restoreProgress += 1 | ||||
|                     showRestoreProgress(restoreProgress, restoreAmount, backupManga.title, errors.size) | ||||
|     private fun restoreMangaNoFetch( | ||||
|         source: Source, | ||||
|         backupManga: Manga, | ||||
|         chapters: List<Chapter>, | ||||
|         categories: List<String>, | ||||
|         history: List<DHistory>, | ||||
|         tracks: List<Track> | ||||
|     ) { | ||||
|         Observable.just(backupManga) | ||||
|             .flatMap { manga -> | ||||
|                 if (!backupManager.restoreChaptersForManga(manga, chapters)) { | ||||
|                     chapterFetchObservable(source, manga, chapters) | ||||
|                         .map { manga } | ||||
|                 } else { | ||||
|                     Observable.just(manga) | ||||
|                 } | ||||
|             } | ||||
|             .doOnNext { | ||||
|                 restoreExtraForManga(it, categories, history, tracks) | ||||
|             } | ||||
|             .flatMap { manga -> | ||||
|                 trackingFetchObservable(manga, tracks) | ||||
|             } | ||||
|             .subscribe() | ||||
|     } | ||||
|  | ||||
|     private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) { | ||||
| @@ -442,21 +399,12 @@ class BackupRestoreService : Service() { | ||||
|      * @return [Observable] that contains manga | ||||
|      */ | ||||
|     private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> { | ||||
|         return backupManager.restoreChapterFetchObservable(source, manga, chapters, throttleManager) | ||||
|                 // If there's any error, return empty update and continue. | ||||
|                 .onErrorReturn { | ||||
|                     // [EXH] | ||||
|                     XLog.w("> Failed to restore chapter!", it) | ||||
|                     XLog.w("> (source.id: %s, source.name: %s, manga.id: %s, manga.url: %s, chapters.size: %s)", | ||||
|                             source.id, | ||||
|                             source.name, | ||||
|                             manga.id, | ||||
|                             manga.url, | ||||
|                             chapters.size) | ||||
|  | ||||
|                     errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                     Pair(emptyList(), emptyList()) | ||||
|                 } | ||||
|         return backupManager.restoreChapterFetchObservable(source, manga, chapters) | ||||
|             // If there's any error, return empty update and continue. | ||||
|             .onErrorReturn { | ||||
|                 errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                 Pair(emptyList(), emptyList()) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -467,20 +415,20 @@ class BackupRestoreService : Service() { | ||||
|      */ | ||||
|     private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> { | ||||
|         return Observable.from(tracks) | ||||
|                 .concatMap { track -> | ||||
|                     val service = trackManager.getService(track.sync_id) | ||||
|                     if (service != null && service.isLogged) { | ||||
|                         service.refresh(track) | ||||
|                                 .doOnNext { db.insertTrack(it).executeAsBlocking() } | ||||
|                                 .onErrorReturn { | ||||
|                                     errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                                     track | ||||
|                                 } | ||||
|                     } else { | ||||
|                         errors.add(Date() to "${manga.title} - ${service?.name} not logged in") | ||||
|                         Observable.empty() | ||||
|                     } | ||||
|             .concatMap { track -> | ||||
|                 val service = trackManager.getService(track.sync_id) | ||||
|                 if (service != null && service.isLogged) { | ||||
|                     service.refresh(track) | ||||
|                         .doOnNext { db.insertTrack(it).executeAsBlocking() } | ||||
|                         .onErrorReturn { | ||||
|                             errors.add(Date() to "${manga.title} - ${it.message}") | ||||
|                             track | ||||
|                         } | ||||
|                 } else { | ||||
|                     errors.add(Date() to "${manga.title} - ${service?.name} not logged in") | ||||
|                     Observable.empty() | ||||
|                 } | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -490,16 +438,33 @@ class BackupRestoreService : Service() { | ||||
|      * @param amount total restoreAmount of manga | ||||
|      * @param title title of restored manga | ||||
|      */ | ||||
|     private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int, | ||||
|                                     content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) { | ||||
|         val intent = Intent(BackupConst.INTENT_FILTER).apply { | ||||
|             putExtra(BackupConst.EXTRA_PROGRESS, progress) | ||||
|             putExtra(BackupConst.EXTRA_AMOUNT, amount) | ||||
|             putExtra(BackupConst.EXTRA_CONTENT, content) | ||||
|             putExtra(BackupConst.EXTRA_ERRORS, errors) | ||||
|             putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG) | ||||
|         } | ||||
|         sendLocalBroadcast(intent) | ||||
|     private fun showRestoreProgress( | ||||
|         progress: Int, | ||||
|         amount: Int, | ||||
|         title: String | ||||
|     ) { | ||||
|         notifier.showRestoreProgress(title, progress, amount) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Write errors to error log | ||||
|      */ | ||||
|     private fun writeErrorLog(): File { | ||||
|         try { | ||||
|             if (errors.isNotEmpty()) { | ||||
|                 val destFile = File(externalCacheDir, "tachiyomi_restore.txt") | ||||
|                 val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault()) | ||||
|  | ||||
|                 destFile.bufferedWriter().use { out -> | ||||
|                     errors.forEach { (date, message) -> | ||||
|                         out.write("[${sdf.format(date)}] $message\n") | ||||
|                     } | ||||
|                 } | ||||
|                 return destFile | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             // Empty | ||||
|         } | ||||
|         return File("") | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,23 +1,25 @@ | ||||
| package eu.kanade.tachiyomi.data.backup.models | ||||
|  | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.* | ||||
|  | ||||
| /** | ||||
|  * Json values | ||||
|  */ | ||||
| object Backup { | ||||
|     const val CURRENT_VERSION = 2 | ||||
|     const val MANGA = "manga" | ||||
|     const val MANGAS = "mangas" | ||||
|     const val TRACK = "track" | ||||
|     const val CHAPTERS = "chapters" | ||||
|     const val CATEGORIES = "categories" | ||||
|     const val HISTORY = "history" | ||||
|     const val VERSION = "version" | ||||
|  | ||||
|     fun getDefaultFilename(): String { | ||||
|         val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date()) | ||||
|         return "tachiyomi_$date.json" | ||||
|     } | ||||
| } | ||||
| package eu.kanade.tachiyomi.data.backup.models | ||||
|  | ||||
| import java.text.SimpleDateFormat | ||||
| import java.util.Date | ||||
| import java.util.Locale | ||||
|  | ||||
| /** | ||||
|  * Json values | ||||
|  */ | ||||
| object Backup { | ||||
|     const val CURRENT_VERSION = 2 | ||||
|     const val MANGA = "manga" | ||||
|     const val MANGAS = "mangas" | ||||
|     const val TRACK = "track" | ||||
|     const val CHAPTERS = "chapters" | ||||
|     const val CATEGORIES = "categories" | ||||
|     const val EXTENSIONS = "extensions" | ||||
|     const val HISTORY = "history" | ||||
|     const val VERSION = "version" | ||||
|  | ||||
|     fun getDefaultFilename(): String { | ||||
|         val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date()) | ||||
|         return "tachiyomi_$date.json" | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,3 +1,3 @@ | ||||
| package eu.kanade.tachiyomi.data.backup.models | ||||
|  | ||||
| data class DHistory(val url: String,val lastRead: Long) | ||||
| data class DHistory(val url: String, val lastRead: Long) | ||||
|   | ||||
| @@ -28,4 +28,4 @@ object CategoryTypeAdapter { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -43,9 +43,7 @@ object ChapterTypeAdapter { | ||||
|                 beginObject() | ||||
|                 while (hasNext()) { | ||||
|                     if (peek() == JsonToken.NAME) { | ||||
|                         val name = nextName() | ||||
|  | ||||
|                         when (name) { | ||||
|                         when (nextName()) { | ||||
|                             URL -> chapter.url = nextString() | ||||
|                             READ -> chapter.read = nextInt() == 1 | ||||
|                             BOOKMARK -> chapter.bookmark = nextInt() == 1 | ||||
| @@ -58,4 +56,4 @@ object ChapterTypeAdapter { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -29,4 +29,4 @@ object HistoryTypeAdapter { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -34,4 +34,4 @@ object MangaTypeAdapter { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -41,9 +41,7 @@ object TrackTypeAdapter { | ||||
|                 beginObject() | ||||
|                 while (hasNext()) { | ||||
|                     if (peek() == JsonToken.NAME) { | ||||
|                         val name = nextName() | ||||
|  | ||||
|                         when (name) { | ||||
|                         when (nextName()) { | ||||
|                             TITLE -> track.title = nextString() | ||||
|                             SYNC -> track.sync_id = nextInt() | ||||
|                             MEDIA -> track.media_id = nextInt() | ||||
| @@ -58,4 +56,4 @@ object TrackTypeAdapter { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -9,15 +9,15 @@ import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.preference.getOrDefault | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.util.DiskUtil | ||||
| import eu.kanade.tachiyomi.util.saveTo | ||||
| import eu.kanade.tachiyomi.util.storage.DiskUtil | ||||
| import eu.kanade.tachiyomi.util.storage.saveTo | ||||
| import java.io.File | ||||
| import java.io.IOException | ||||
| import okhttp3.Response | ||||
| import okio.buffer | ||||
| import okio.sink | ||||
| import rx.Observable | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
| import java.io.IOException | ||||
|  | ||||
| /** | ||||
|  * Class used to create chapter cache | ||||
| @@ -29,6 +29,7 @@ import java.io.IOException | ||||
|  * @constructor creates an instance of the chapter cache. | ||||
|  */ | ||||
| class ChapterCache(private val context: Context) { | ||||
|  | ||||
|     companion object { | ||||
|         /** Name of cache directory.  */ | ||||
|         const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache" | ||||
| @@ -96,16 +97,17 @@ class ChapterCache(private val context: Context) { | ||||
|      */ | ||||
|     fun removeFileFromCache(file: String): Boolean { | ||||
|         // Make sure we don't delete the journal file (keeps track of cache). | ||||
|         if (file == "journal" || file.startsWith("journal.")) | ||||
|         if (file == "journal" || file.startsWith("journal.")) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|         return try { | ||||
|             // Remove the extension from the file to get the key of the cache | ||||
|             val key = file.substringBeforeLast(".") | ||||
|             // Remove file from cache. | ||||
|             return diskCache.remove(key) | ||||
|             diskCache.remove(key) | ||||
|         } catch (e: Exception) { | ||||
|             return false | ||||
|             false | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -154,7 +156,6 @@ class ChapterCache(private val context: Context) { | ||||
|             diskCache.flush() | ||||
|             editor.commit() | ||||
|             editor.abortUnlessCommitted() | ||||
|  | ||||
|         } catch (e: Exception) { | ||||
|             // Ignore. | ||||
|         } finally { | ||||
| @@ -169,10 +170,10 @@ class ChapterCache(private val context: Context) { | ||||
|      * @return true if in cache otherwise false. | ||||
|      */ | ||||
|     fun isImageInCache(imageUrl: String): Boolean { | ||||
|         try { | ||||
|             return diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)) != null | ||||
|         return try { | ||||
|             diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)) != null | ||||
|         } catch (e: IOException) { | ||||
|             return false | ||||
|             false | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -190,7 +191,7 @@ class ChapterCache(private val context: Context) { | ||||
|  | ||||
|     /** | ||||
|      * Add image to cache. | ||||
|      *  | ||||
|      * | ||||
|      * @param imageUrl url of image. | ||||
|      * @param response http response from page. | ||||
|      * @throws IOException image error. | ||||
| @@ -220,4 +221,3 @@ class ChapterCache(private val context: Context) { | ||||
|         return "${chapter.manga_id}${chapter.url}" | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| package eu.kanade.tachiyomi.data.cache | ||||
|  | ||||
| import android.content.Context | ||||
| import eu.kanade.tachiyomi.util.DiskUtil | ||||
| import eu.kanade.tachiyomi.util.storage.DiskUtil | ||||
| import java.io.File | ||||
| import java.io.IOException | ||||
| import java.io.InputStream | ||||
| @@ -20,8 +20,8 @@ class CoverCache(private val context: Context) { | ||||
|     /** | ||||
|      * Cache directory used for cache management. | ||||
|      */ | ||||
|     private val cacheDir = context.getExternalFilesDir("covers") ?: | ||||
|             File(context.filesDir, "covers").also { it.mkdirs() } | ||||
|     private val cacheDir = context.getExternalFilesDir("covers") | ||||
|         ?: File(context.filesDir, "covers").also { it.mkdirs() } | ||||
|  | ||||
|     /** | ||||
|      * Returns the cover from cache. | ||||
| @@ -37,7 +37,7 @@ class CoverCache(private val context: Context) { | ||||
|      * Copy the given stream to this cache. | ||||
|      * | ||||
|      * @param thumbnailUrl url of the thumbnail. | ||||
|      * @param inputStream  the stream to copy. | ||||
|      * @param inputStream the stream to copy. | ||||
|      * @throws IOException if there's any error. | ||||
|      */ | ||||
|     @Throws(IOException::class) | ||||
| @@ -56,12 +56,12 @@ class CoverCache(private val context: Context) { | ||||
|      */ | ||||
|     fun deleteFromCache(thumbnailUrl: String?): Boolean { | ||||
|         // Check if url is empty. | ||||
|         if (thumbnailUrl.isNullOrEmpty()) | ||||
|         if (thumbnailUrl.isNullOrEmpty()) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         // Remove file. | ||||
|         val file = getCoverFile(thumbnailUrl) | ||||
|         return file.exists() && file.delete() | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -3,46 +3,51 @@ package eu.kanade.tachiyomi.data.database | ||||
| import android.content.Context | ||||
| import androidx.sqlite.db.SupportSQLiteOpenHelper | ||||
| import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite | ||||
| import eu.kanade.tachiyomi.data.database.mappers.* | ||||
| import eu.kanade.tachiyomi.data.database.models.* | ||||
| import eu.kanade.tachiyomi.data.database.queries.* | ||||
| import exh.metadata.sql.mappers.SearchMetadataTypeMapping | ||||
| import exh.metadata.sql.mappers.SearchTagTypeMapping | ||||
| import exh.metadata.sql.mappers.SearchTitleTypeMapping | ||||
| import exh.metadata.sql.models.SearchMetadata | ||||
| import exh.metadata.sql.models.SearchTag | ||||
| import exh.metadata.sql.models.SearchTitle | ||||
| import exh.metadata.sql.queries.SearchMetadataQueries | ||||
| import exh.metadata.sql.queries.SearchTagQueries | ||||
| import exh.metadata.sql.queries.SearchTitleQueries | ||||
| import eu.kanade.tachiyomi.data.database.mappers.CategoryTypeMapping | ||||
| import eu.kanade.tachiyomi.data.database.mappers.ChapterTypeMapping | ||||
| import eu.kanade.tachiyomi.data.database.mappers.HistoryTypeMapping | ||||
| import eu.kanade.tachiyomi.data.database.mappers.MangaCategoryTypeMapping | ||||
| import eu.kanade.tachiyomi.data.database.mappers.MangaTypeMapping | ||||
| import eu.kanade.tachiyomi.data.database.mappers.TrackTypeMapping | ||||
| import eu.kanade.tachiyomi.data.database.models.Category | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.History | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.database.models.MangaCategory | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.database.queries.CategoryQueries | ||||
| import eu.kanade.tachiyomi.data.database.queries.ChapterQueries | ||||
| import eu.kanade.tachiyomi.data.database.queries.HistoryQueries | ||||
| import eu.kanade.tachiyomi.data.database.queries.MangaCategoryQueries | ||||
| import eu.kanade.tachiyomi.data.database.queries.MangaQueries | ||||
| import eu.kanade.tachiyomi.data.database.queries.TrackQueries | ||||
| import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory | ||||
|  | ||||
| /** | ||||
|  * This class provides operations to manage the database through its interfaces. | ||||
|  */ | ||||
| open class DatabaseHelper(context: Context) | ||||
|     : MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries, | ||||
|         /* EXH --> */ SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */ | ||||
| { | ||||
| open class DatabaseHelper(context: Context) : | ||||
|     MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries, /* EXH --> */ SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */ { | ||||
|  | ||||
|     private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) | ||||
|             .name(DbOpenCallback.DATABASE_NAME) | ||||
|             .callback(DbOpenCallback()) | ||||
|             .build() | ||||
|  | ||||
|     override val db = DefaultStorIOSQLite.builder() | ||||
|             .sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration)) | ||||
|             .addTypeMapping(Manga::class.java, MangaTypeMapping()) | ||||
|             .addTypeMapping(Chapter::class.java, ChapterTypeMapping()) | ||||
|             .addTypeMapping(Track::class.java, TrackTypeMapping()) | ||||
|             .addTypeMapping(Category::class.java, CategoryTypeMapping()) | ||||
|             .addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping()) | ||||
|             .addTypeMapping(History::class.java, HistoryTypeMapping()) | ||||
|             // EXH --> | ||||
|             .addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping()) | ||||
|             .addTypeMapping(SearchTag::class.java, SearchTagTypeMapping()) | ||||
|             .addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping()) | ||||
|             // EXH <-- | ||||
|             .build() | ||||
|         .sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration)) | ||||
|         .addTypeMapping(Manga::class.java, MangaTypeMapping()) | ||||
|         .addTypeMapping(Chapter::class.java, ChapterTypeMapping()) | ||||
|         .addTypeMapping(Track::class.java, TrackTypeMapping()) | ||||
|         .addTypeMapping(Category::class.java, CategoryTypeMapping()) | ||||
|         .addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping()) | ||||
|         .addTypeMapping(History::class.java, HistoryTypeMapping()) | ||||
|         // EXH --> | ||||
|         .addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping()) | ||||
|         .addTypeMapping(SearchTag::class.java, SearchTagTypeMapping()) | ||||
|         .addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping()) | ||||
|         // EXH <-- | ||||
|         .build() | ||||
|  | ||||
|     inline fun inTransaction(block: () -> Unit) = db.inTransaction(block) | ||||
|  | ||||
|   | ||||
| @@ -22,4 +22,3 @@ inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T { | ||||
|         lowLevel().endTransaction() | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,12 @@ package eu.kanade.tachiyomi.data.database | ||||
|  | ||||
| import androidx.sqlite.db.SupportSQLiteDatabase | ||||
| import androidx.sqlite.db.SupportSQLiteOpenHelper | ||||
| import eu.kanade.tachiyomi.data.database.tables.* | ||||
| import eu.kanade.tachiyomi.data.database.tables.CategoryTable | ||||
| import eu.kanade.tachiyomi.data.database.tables.ChapterTable | ||||
| import eu.kanade.tachiyomi.data.database.tables.HistoryTable | ||||
| import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable | ||||
| import eu.kanade.tachiyomi.data.database.tables.MangaTable | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable | ||||
| import exh.metadata.sql.tables.SearchMetadataTable | ||||
| import exh.metadata.sql.tables.SearchTagTable | ||||
| import exh.metadata.sql.tables.SearchTitleTable | ||||
| @@ -18,7 +23,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { | ||||
|         /** | ||||
|          * Version of the database. | ||||
|          */ | ||||
|         const val DATABASE_VERSION = 9 // [EXH] | ||||
|         const val DATABASE_VERSION = 0 // [SY] | ||||
|     } | ||||
|  | ||||
|     override fun onCreate(db: SupportSQLiteDatabase) = with(db) { | ||||
| @@ -51,54 +56,18 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { | ||||
|     } | ||||
|  | ||||
|     override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) { | ||||
|         if (oldVersion < 2) { | ||||
|         if (oldVersion < 0) { | ||||
|             db.execSQL(ChapterTable.sourceOrderUpdateQuery) | ||||
|  | ||||
|             // Fix kissmanga covers after supporting cloudflare | ||||
|             db.execSQL("""UPDATE mangas SET thumbnail_url = | ||||
|                     REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""") | ||||
|             db.execSQL( | ||||
|                 """UPDATE mangas SET thumbnail_url = | ||||
|                     REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""" | ||||
|             ) | ||||
|         } | ||||
|         if (oldVersion < 3) { | ||||
|             // Initialize history tables | ||||
|             db.execSQL(HistoryTable.createTableQuery) | ||||
|             db.execSQL(HistoryTable.createChapterIdIndexQuery) | ||||
|         } | ||||
|         if (oldVersion < 4) { | ||||
|             db.execSQL(ChapterTable.bookmarkUpdateQuery) | ||||
|         } | ||||
|         if (oldVersion < 5) { | ||||
|             db.execSQL(ChapterTable.addScanlator) | ||||
|         } | ||||
|         if (oldVersion < 6) { | ||||
|             db.execSQL(TrackTable.addTrackingUrl) | ||||
|         } | ||||
|         if (oldVersion < 7) { | ||||
|             db.execSQL(TrackTable.addLibraryId) | ||||
|         } | ||||
|         if (oldVersion < 8) { | ||||
|             db.execSQL("DROP INDEX IF EXISTS mangas_favorite_index") | ||||
|             db.execSQL(MangaTable.createLibraryIndexQuery) | ||||
|             db.execSQL(ChapterTable.createUnreadChaptersIndexQuery) | ||||
|         } | ||||
|         // EXH --> | ||||
|         if (oldVersion < 9) { | ||||
|             db.execSQL(SearchMetadataTable.createTableQuery) | ||||
|             db.execSQL(SearchTagTable.createTableQuery) | ||||
|             db.execSQL(SearchTitleTable.createTableQuery) | ||||
|  | ||||
|             db.execSQL(SearchMetadataTable.createUploaderIndexQuery) | ||||
|             db.execSQL(SearchMetadataTable.createIndexedExtraIndexQuery) | ||||
|             db.execSQL(SearchTagTable.createMangaIdIndexQuery) | ||||
|             db.execSQL(SearchTagTable.createNamespaceNameIndexQuery) | ||||
|             db.execSQL(SearchTitleTable.createMangaIdIndexQuery) | ||||
|             db.execSQL(SearchTitleTable.createTitleIndexQuery) | ||||
|         } | ||||
|         // Remember to increment any Tachiyomi database upgrades after this | ||||
|         // EXH <-- | ||||
|     } | ||||
|  | ||||
|     override fun onConfigure(db: SupportSQLiteDatabase) { | ||||
|         db.setForeignKeyConstraintsEnabled(true) | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -5,5 +5,4 @@ import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite | ||||
| interface DbProvider { | ||||
|  | ||||
|     val db: DefaultStorIOSQLite | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -18,22 +18,22 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER | ||||
| import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE | ||||
|  | ||||
| class CategoryTypeMapping : SQLiteTypeMapping<Category>( | ||||
|         CategoryPutResolver(), | ||||
|         CategoryGetResolver(), | ||||
|         CategoryDeleteResolver() | ||||
|     CategoryPutResolver(), | ||||
|     CategoryGetResolver(), | ||||
|     CategoryDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class CategoryPutResolver : DefaultPutResolver<Category>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: Category) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: Category) = ContentValues(4).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -56,8 +56,8 @@ class CategoryGetResolver : DefaultGetResolver<Category>() { | ||||
| class CategoryDeleteResolver : DefaultDeleteResolver<Category>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -26,22 +26,22 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL | ||||
| import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE | ||||
|  | ||||
| class ChapterTypeMapping : SQLiteTypeMapping<Chapter>( | ||||
|         ChapterPutResolver(), | ||||
|         ChapterGetResolver(), | ||||
|         ChapterDeleteResolver() | ||||
|     ChapterPutResolver(), | ||||
|     ChapterGetResolver(), | ||||
|     ChapterDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class ChapterPutResolver : DefaultPutResolver<Chapter>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -80,9 +80,8 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() { | ||||
| class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -18,22 +18,22 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ | ||||
| import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE | ||||
|  | ||||
| class HistoryTypeMapping : SQLiteTypeMapping<History>( | ||||
|         HistoryPutResolver(), | ||||
|         HistoryGetResolver(), | ||||
|         HistoryDeleteResolver() | ||||
|     HistoryPutResolver(), | ||||
|     HistoryGetResolver(), | ||||
|     HistoryDeleteResolver() | ||||
| ) | ||||
|  | ||||
| open class HistoryPutResolver : DefaultPutResolver<History>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: History) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: History) = ContentValues(4).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -56,8 +56,8 @@ class HistoryGetResolver : DefaultGetResolver<History>() { | ||||
| class HistoryDeleteResolver : DefaultDeleteResolver<History>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -16,22 +16,22 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_MANGA_ID | ||||
| import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE | ||||
|  | ||||
| class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>( | ||||
|         MangaCategoryPutResolver(), | ||||
|         MangaCategoryGetResolver(), | ||||
|         MangaCategoryDeleteResolver() | ||||
|     MangaCategoryPutResolver(), | ||||
|     MangaCategoryGetResolver(), | ||||
|     MangaCategoryDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -52,8 +52,8 @@ class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() { | ||||
| class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -29,22 +29,22 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER | ||||
| import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE | ||||
|  | ||||
| class MangaTypeMapping : SQLiteTypeMapping<Manga>( | ||||
|         MangaPutResolver(), | ||||
|         MangaGetResolver(), | ||||
|         MangaDeleteResolver() | ||||
|     MangaPutResolver(), | ||||
|     MangaGetResolver(), | ||||
|     MangaDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class MangaPutResolver : DefaultPutResolver<Manga>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: Manga) = ContentValues(15).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -95,8 +95,8 @@ open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver | ||||
| class MangaDeleteResolver : DefaultDeleteResolver<Manga>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -11,12 +11,14 @@ import com.pushtorefresh.storio.sqlite.queries.InsertQuery | ||||
| import com.pushtorefresh.storio.sqlite.queries.UpdateQuery | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.database.models.TrackImpl | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_FINISH_DATE | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LIBRARY_ID | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MEDIA_ID | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_START_DATE | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE | ||||
| @@ -25,22 +27,22 @@ import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TRACKING_URL | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE | ||||
|  | ||||
| class TrackTypeMapping : SQLiteTypeMapping<Track>( | ||||
|         TrackPutResolver(), | ||||
|         TrackGetResolver(), | ||||
|         TrackDeleteResolver() | ||||
|     TrackPutResolver(), | ||||
|     TrackGetResolver(), | ||||
|     TrackDeleteResolver() | ||||
| ) | ||||
|  | ||||
| class TrackPutResolver : DefaultPutResolver<Track>() { | ||||
|  | ||||
|     override fun mapToInsertQuery(obj: Track) = InsertQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
|  | ||||
|     override fun mapToContentValues(obj: Track) = ContentValues(10).apply { | ||||
|         put(COL_ID, obj.id) | ||||
| @@ -54,7 +56,8 @@ class TrackPutResolver : DefaultPutResolver<Track>() { | ||||
|         put(COL_STATUS, obj.status) | ||||
|         put(COL_TRACKING_URL, obj.tracking_url) | ||||
|         put(COL_SCORE, obj.score) | ||||
|  | ||||
|         put(COL_START_DATE, obj.started_reading_date) | ||||
|         put(COL_FINISH_DATE, obj.finished_reading_date) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -72,14 +75,16 @@ class TrackGetResolver : DefaultGetResolver<Track>() { | ||||
|         status = cursor.getInt(cursor.getColumnIndex(COL_STATUS)) | ||||
|         score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE)) | ||||
|         tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL)) | ||||
|         started_reading_date = cursor.getLong(cursor.getColumnIndex(COL_START_DATE)) | ||||
|         finished_reading_date = cursor.getLong(cursor.getColumnIndex(COL_FINISH_DATE)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| class TrackDeleteResolver : DefaultDeleteResolver<Track>() { | ||||
|  | ||||
|     override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder() | ||||
|             .table(TABLE) | ||||
|             .where("$COL_ID = ?") | ||||
|             .whereArgs(obj.id) | ||||
|             .build() | ||||
|         .table(TABLE) | ||||
|         .where("$COL_ID = ?") | ||||
|         .whereArgs(obj.id) | ||||
|         .build() | ||||
| } | ||||
|   | ||||
| @@ -23,5 +23,4 @@ interface Category : Serializable { | ||||
|  | ||||
|         fun createDefault(): Category = create("Default").apply { id = 0 } | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -22,5 +22,4 @@ class CategoryImpl : Category { | ||||
|     override fun hashCode(): Int { | ||||
|         return name.hashCode() | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -37,5 +37,4 @@ class ChapterImpl : Chapter { | ||||
|     override fun hashCode(): Int { | ||||
|         return url.hashCode() | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -35,7 +35,7 @@ interface History : Serializable { | ||||
|          * @param chapter chapter object | ||||
|          * @return history object | ||||
|          */ | ||||
|         fun create(chapter: Chapter): History =  HistoryImpl().apply { | ||||
|         fun create(chapter: Chapter): History = HistoryImpl().apply { | ||||
|             this.chapter_id = chapter.id!! | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -5,5 +5,4 @@ class LibraryManga : MangaImpl() { | ||||
|     var unread: Int = 0 | ||||
|  | ||||
|     var category: Int = 0 | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -28,6 +28,10 @@ interface Manga : SManga { | ||||
|         return chapter_flags and SORT_MASK == SORT_DESC | ||||
|     } | ||||
|  | ||||
|     fun getGenres(): List<String>? { | ||||
|         return genre?.split(", ")?.map { it.trim() } | ||||
|     } | ||||
|  | ||||
|     // Used to display the chapter's title one way or another | ||||
|     var displayMode: Int | ||||
|         get() = chapter_flags and DISPLAY_MASK | ||||
| @@ -88,5 +92,4 @@ interface Manga : SManga { | ||||
|             this.source = source | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -17,5 +17,4 @@ class MangaCategory { | ||||
|             return mc | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -5,6 +5,6 @@ package eu.kanade.tachiyomi.data.database.models | ||||
|  * | ||||
|  * @param manga object containing manga | ||||
|  * @param chapter object containing chater | ||||
|  * @param history      object containing history | ||||
|  * @param history object containing history | ||||
|  */ | ||||
| data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History) | ||||
|   | ||||
| @@ -39,11 +39,9 @@ open class MangaImpl : Manga { | ||||
|         val manga = other as Manga | ||||
|  | ||||
|         return url == manga.url | ||||
|  | ||||
|     } | ||||
|  | ||||
|     override fun hashCode(): Int { | ||||
|         return url.hashCode() | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -24,12 +24,18 @@ interface Track : Serializable { | ||||
|  | ||||
|     var status: Int | ||||
|  | ||||
|     var started_reading_date: Long | ||||
|  | ||||
|     var finished_reading_date: Long | ||||
|  | ||||
|     var tracking_url: String | ||||
|  | ||||
|     fun copyPersonalFrom(other: Track) { | ||||
|         last_chapter_read = other.last_chapter_read | ||||
|         score = other.score | ||||
|         status = other.status | ||||
|         started_reading_date = other.started_reading_date | ||||
|         finished_reading_date = other.finished_reading_date | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
| @@ -37,5 +43,4 @@ interface Track : Serializable { | ||||
|             sync_id = serviceId | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -22,6 +22,10 @@ class TrackImpl : Track { | ||||
|  | ||||
|     override var status: Int = 0 | ||||
|  | ||||
|     override var started_reading_date: Long = 0 | ||||
|  | ||||
|     override var finished_reading_date: Long = 0 | ||||
|  | ||||
|     override var tracking_url: String = "" | ||||
|  | ||||
|     override fun equals(other: Any?): Boolean { | ||||
| @@ -41,5 +45,4 @@ class TrackImpl : Track { | ||||
|         result = 31 * result + media_id | ||||
|         return result | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -10,20 +10,24 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable | ||||
| interface CategoryQueries : DbProvider { | ||||
|  | ||||
|     fun getCategories() = db.get() | ||||
|             .listOfObjects(Category::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(CategoryTable.TABLE) | ||||
|                     .orderBy(CategoryTable.COL_ORDER) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Category::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(CategoryTable.TABLE) | ||||
|                 .orderBy(CategoryTable.COL_ORDER) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getCategoriesForManga(manga: Manga) = db.get() | ||||
|             .listOfObjects(Category::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getCategoriesForMangaQuery()) | ||||
|                     .args(manga.id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Category::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getCategoriesForMangaQuery()) | ||||
|                 .args(manga.id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun insertCategory(category: Category) = db.put().`object`(category).prepare() | ||||
|  | ||||
| @@ -32,5 +36,4 @@ interface CategoryQueries : DbProvider { | ||||
|     fun deleteCategory(category: Category) = db.delete().`object`(category).prepare() | ||||
|  | ||||
|     fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare() | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver | ||||
| import eu.kanade.tachiyomi.data.database.tables.ChapterTable | ||||
| import java.util.* | ||||
| import java.util.Date | ||||
|  | ||||
| interface ChapterQueries : DbProvider { | ||||
|  | ||||
| @@ -27,32 +27,49 @@ interface ChapterQueries : DbProvider { | ||||
|             .prepare() | ||||
|  | ||||
|     fun getRecentChapters(date: Date) = db.get() | ||||
|             .listOfObjects(MangaChapter::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getRecentsQuery()) | ||||
|                     .args(date.time) | ||||
|                     .observesTables(ChapterTable.TABLE) | ||||
|                     .build()) | ||||
|             .withGetResolver(MangaChapterGetResolver.INSTANCE) | ||||
|             .prepare() | ||||
|         .listOfObjects(MangaChapter::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getRecentsQuery()) | ||||
|                 .args(date.time) | ||||
|                 .observesTables(ChapterTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .withGetResolver(MangaChapterGetResolver.INSTANCE) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getChapter(id: Long) = db.get() | ||||
|             .`object`(Chapter::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(ChapterTable.TABLE) | ||||
|                     .where("${ChapterTable.COL_ID} = ?") | ||||
|                     .whereArgs(id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(Chapter::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(ChapterTable.TABLE) | ||||
|                 .where("${ChapterTable.COL_ID} = ?") | ||||
|                 .whereArgs(id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getChapter(url: String) = db.get() | ||||
|             .`object`(Chapter::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(ChapterTable.TABLE) | ||||
|                     .where("${ChapterTable.COL_URL} = ?") | ||||
|                     .whereArgs(url) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(Chapter::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(ChapterTable.TABLE) | ||||
|                 .where("${ChapterTable.COL_URL} = ?") | ||||
|                 .whereArgs(url) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getChapter(url: String, mangaId: Long) = db.get() | ||||
|         .`object`(Chapter::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(ChapterTable.TABLE) | ||||
|                 .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") | ||||
|                 .whereArgs(url, mangaId) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getChapters(url: String) = db.get() | ||||
|             .listOfObjects(Chapter::class.java) | ||||
| @@ -73,23 +90,22 @@ interface ChapterQueries : DbProvider { | ||||
|     fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare() | ||||
|  | ||||
|     fun updateChaptersBackup(chapters: List<Chapter>) = db.put() | ||||
|             .objects(chapters) | ||||
|             .withPutResolver(ChapterBackupPutResolver()) | ||||
|             .prepare() | ||||
|         .objects(chapters) | ||||
|         .withPutResolver(ChapterBackupPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateChapterProgress(chapter: Chapter) = db.put() | ||||
|             .`object`(chapter) | ||||
|             .withPutResolver(ChapterProgressPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(chapter) | ||||
|         .withPutResolver(ChapterProgressPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateChaptersProgress(chapters: List<Chapter>) = db.put() | ||||
|             .objects(chapters) | ||||
|             .withPutResolver(ChapterProgressPutResolver()) | ||||
|             .prepare() | ||||
|         .objects(chapters) | ||||
|         .withPutResolver(ChapterProgressPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put() | ||||
|             .objects(chapters) | ||||
|             .withPutResolver(ChapterSourceOrderPutResolver()) | ||||
|             .prepare() | ||||
|  | ||||
| } | ||||
|         .objects(chapters) | ||||
|         .withPutResolver(ChapterSourceOrderPutResolver()) | ||||
|         .prepare() | ||||
| } | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver | ||||
| import eu.kanade.tachiyomi.data.database.tables.HistoryTable | ||||
| import java.util.* | ||||
| import java.util.Date | ||||
|  | ||||
| interface HistoryQueries : DbProvider { | ||||
|  | ||||
| @@ -23,32 +23,38 @@ interface HistoryQueries : DbProvider { | ||||
|      * @param date recent date range | ||||
|      */ | ||||
|     fun getRecentManga(date: Date) = db.get() | ||||
|             .listOfObjects(MangaChapterHistory::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getRecentMangasQuery()) | ||||
|                     .args(date.time) | ||||
|                     .observesTables(HistoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) | ||||
|             .prepare() | ||||
|         .listOfObjects(MangaChapterHistory::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getRecentMangasQuery()) | ||||
|                 .args(date.time) | ||||
|                 .observesTables(HistoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getHistoryByMangaId(mangaId: Long) = db.get() | ||||
|             .listOfObjects(History::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getHistoryByMangaId()) | ||||
|                     .args(mangaId) | ||||
|                     .observesTables(HistoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(History::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getHistoryByMangaId()) | ||||
|                 .args(mangaId) | ||||
|                 .observesTables(HistoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getHistoryByChapterUrl(chapterUrl: String) = db.get() | ||||
|             .`object`(History::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getHistoryByChapterUrl()) | ||||
|                     .args(chapterUrl) | ||||
|                     .observesTables(HistoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(History::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getHistoryByChapterUrl()) | ||||
|                 .args(chapterUrl) | ||||
|                 .observesTables(HistoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     /** | ||||
|      * Updates the history last read. | ||||
| @@ -56,9 +62,9 @@ interface HistoryQueries : DbProvider { | ||||
|      * @param history history object | ||||
|      */ | ||||
|     fun updateHistoryLastRead(history: History) = db.put() | ||||
|             .`object`(history) | ||||
|             .withPutResolver(HistoryLastReadPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(history) | ||||
|         .withPutResolver(HistoryLastReadPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     /** | ||||
|      * Updates the history last read. | ||||
| @@ -66,21 +72,25 @@ interface HistoryQueries : DbProvider { | ||||
|      * @param historyList history object list | ||||
|      */ | ||||
|     fun updateHistoryLastRead(historyList: List<History>) = db.put() | ||||
|             .objects(historyList) | ||||
|             .withPutResolver(HistoryLastReadPutResolver()) | ||||
|             .prepare() | ||||
|         .objects(historyList) | ||||
|         .withPutResolver(HistoryLastReadPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun deleteHistory() = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(HistoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(HistoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun deleteHistoryNoLastRead() = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(HistoryTable.TABLE) | ||||
|                     .where("${HistoryTable.COL_LAST_READ} = ?") | ||||
|                     .whereArgs(0) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(HistoryTable.TABLE) | ||||
|                 .where("${HistoryTable.COL_LAST_READ} = ?") | ||||
|                 .whereArgs(0) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
| } | ||||
|   | ||||
| @@ -15,12 +15,14 @@ interface MangaCategoryQueries : DbProvider { | ||||
|     fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare() | ||||
|  | ||||
|     fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(MangaCategoryTable.TABLE) | ||||
|                     .where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})") | ||||
|                     .whereArgs(*mangas.map { it.id }.toTypedArray()) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(MangaCategoryTable.TABLE) | ||||
|                 .where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})") | ||||
|                 .whereArgs(*mangas.map { it.id }.toTypedArray()) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) { | ||||
|         db.inTransaction { | ||||
| @@ -32,5 +34,4 @@ interface MangaCategoryQueries : DbProvider { | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -6,7 +6,12 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery | ||||
| import eu.kanade.tachiyomi.data.database.DbProvider | ||||
| import eu.kanade.tachiyomi.data.database.models.LibraryManga | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.* | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver | ||||
| import eu.kanade.tachiyomi.data.database.resolvers.MangaViewerPutResolver | ||||
| import eu.kanade.tachiyomi.data.database.tables.CategoryTable | ||||
| import eu.kanade.tachiyomi.data.database.tables.ChapterTable | ||||
| import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable | ||||
| @@ -16,107 +21,140 @@ import exh.metadata.sql.tables.SearchMetadataTable | ||||
| interface MangaQueries : DbProvider { | ||||
|  | ||||
|     fun getMangas() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getLibraryMangas() = db.get() | ||||
|             .listOfObjects(LibraryManga::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(libraryQuery) | ||||
|                     .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) | ||||
|                     .build()) | ||||
|             .withGetResolver(LibraryMangaGetResolver.INSTANCE) | ||||
|             .prepare() | ||||
|         .listOfObjects(LibraryManga::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(libraryQuery) | ||||
|                 .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .withGetResolver(LibraryMangaGetResolver.INSTANCE) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getFavoriteMangas() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .where("${MangaTable.COL_FAVORITE} = ?") | ||||
|                     .whereArgs(1) | ||||
|                     .orderBy(MangaTable.COL_TITLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .where("${MangaTable.COL_FAVORITE} = ?") | ||||
|                 .whereArgs(1) | ||||
|                 .orderBy(MangaTable.COL_TITLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getManga(url: String, sourceId: Long) = db.get() | ||||
|             .`object`(Manga::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?") | ||||
|                     .whereArgs(url, sourceId) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(Manga::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?") | ||||
|                 .whereArgs(url, sourceId) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getManga(id: Long) = db.get() | ||||
|             .`object`(Manga::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .where("${MangaTable.COL_ID} = ?") | ||||
|                     .whereArgs(id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .`object`(Manga::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .where("${MangaTable.COL_ID} = ?") | ||||
|                 .whereArgs(id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun insertManga(manga: Manga) = db.put().`object`(manga).prepare() | ||||
|  | ||||
|     fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare() | ||||
|  | ||||
|     fun updateFlags(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaFlagsPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaFlagsPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateLastUpdated(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaLastUpdatedPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaLastUpdatedPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateMangaFavorite(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaFavoritePutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaFavoritePutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateMangaViewer(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaViewerPutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaViewerPutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun updateMangaTitle(manga: Manga) = db.put() | ||||
|             .`object`(manga) | ||||
|             .withPutResolver(MangaTitlePutResolver()) | ||||
|             .prepare() | ||||
|         .`object`(manga) | ||||
|         .withPutResolver(MangaTitlePutResolver()) | ||||
|         .prepare() | ||||
|  | ||||
|     fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() | ||||
|  | ||||
|     fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare() | ||||
|  | ||||
|     fun deleteMangasNotInLibrary() = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .where("${MangaTable.COL_FAVORITE} = ?") | ||||
|                     .whereArgs(0) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .where("${MangaTable.COL_FAVORITE} = ?") | ||||
|                 .whereArgs(0) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun deleteMangas() = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(MangaTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getLastReadManga() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|                     .query(getLastReadMangaQuery()) | ||||
|                     .observesTables(MangaTable.TABLE) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getLastReadMangaQuery()) | ||||
|                 .observesTables(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java) | ||||
|             .withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare(); | ||||
|     fun getTotalChapterManga() = db.get() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getTotalChapterMangaQuery()) | ||||
|                 .observesTables(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun getLatestChapterManga() = db.get() | ||||
|         .listOfObjects(Manga::class.java) | ||||
|         .withQuery( | ||||
|             RawQuery.builder() | ||||
|                 .query(getLatestChapterMangaQuery()) | ||||
|                 .observesTables(MangaTable.TABLE) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|          | ||||
|     fun getMangaWithMetadata() = db.get() | ||||
|             .listOfObjects(Manga::class.java) | ||||
|             .withQuery(RawQuery.builder() | ||||
|   | ||||
| @@ -9,7 +9,8 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga | ||||
| /** | ||||
|  * Query to get the manga from the library, with their categories and unread count. | ||||
|  */ | ||||
| val libraryQuery = """ | ||||
| val libraryQuery = | ||||
|     """ | ||||
|     SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY} | ||||
|     FROM ( | ||||
|         SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD} | ||||
| @@ -33,7 +34,8 @@ val libraryQuery = """ | ||||
| /** | ||||
|  * Query to get the recent chapters of manga from the library up to a date. | ||||
|  */ | ||||
| fun getRecentsQuery() = """ | ||||
| fun getRecentsQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE} | ||||
|     ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} | ||||
|     WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ? | ||||
| @@ -47,7 +49,8 @@ fun getRecentsQuery() = """ | ||||
|  * and are read after the given time period | ||||
|  * @return return limit is 25 | ||||
|  */ | ||||
| fun getRecentMangasQuery() = """ | ||||
| fun getRecentMangasQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.* | ||||
|     FROM ${Manga.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -65,7 +68,8 @@ fun getRecentMangasQuery() = """ | ||||
|     LIMIT 25 | ||||
| """ | ||||
|  | ||||
| fun getHistoryByMangaId() = """ | ||||
| fun getHistoryByMangaId() = | ||||
|     """ | ||||
|     SELECT ${History.TABLE}.* | ||||
|     FROM ${History.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -73,7 +77,8 @@ fun getHistoryByMangaId() = """ | ||||
|     WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID} | ||||
| """ | ||||
|  | ||||
| fun getHistoryByChapterUrl() = """ | ||||
| fun getHistoryByChapterUrl() = | ||||
|     """ | ||||
|     SELECT ${History.TABLE}.* | ||||
|     FROM ${History.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -81,7 +86,8 @@ fun getHistoryByChapterUrl() = """ | ||||
|     WHERE ${Chapter.TABLE}.${Chapter.COL_URL} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID} | ||||
| """ | ||||
|  | ||||
| fun getLastReadMangaQuery() = """ | ||||
| fun getLastReadMangaQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max | ||||
|     FROM ${Manga.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -93,7 +99,8 @@ fun getLastReadMangaQuery() = """ | ||||
|     ORDER BY max DESC | ||||
| """ | ||||
|  | ||||
| fun getTotalChapterMangaQuery()= """ | ||||
| fun getTotalChapterMangaQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.* | ||||
|     FROM ${Manga.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
| @@ -102,12 +109,23 @@ fun getTotalChapterMangaQuery()= """ | ||||
|     ORDER by COUNT(*) | ||||
| """ | ||||
|  | ||||
| fun getLatestChapterMangaQuery() = | ||||
|     """ | ||||
|     SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD}) AS max | ||||
|     FROM ${Manga.TABLE} | ||||
|     JOIN ${Chapter.TABLE} | ||||
|     ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} | ||||
|     GROUP BY ${Manga.TABLE}.${Manga.COL_ID} | ||||
|     ORDER by max DESC | ||||
| """ | ||||
|  | ||||
| /** | ||||
|  * Query to get the categories for a manga. | ||||
|  */ | ||||
| fun getCategoriesForMangaQuery() = """ | ||||
| fun getCategoriesForMangaQuery() = | ||||
|     """ | ||||
|     SELECT ${Category.TABLE}.* FROM ${Category.TABLE} | ||||
|     JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} = | ||||
|     ${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID} | ||||
|     WHERE ${MangaCategory.COL_MANGA_ID} = ? | ||||
| """ | ||||
| """ | ||||
|   | ||||
| @@ -1,34 +1,37 @@ | ||||
| package eu.kanade.tachiyomi.data.database.queries | ||||
|  | ||||
| import com.pushtorefresh.storio.sqlite.queries.DeleteQuery | ||||
| import com.pushtorefresh.storio.sqlite.queries.Query | ||||
| import eu.kanade.tachiyomi.data.database.DbProvider | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable | ||||
| import eu.kanade.tachiyomi.data.track.TrackService | ||||
|  | ||||
| interface TrackQueries : DbProvider { | ||||
|  | ||||
|     fun getTracks(manga: Manga) = db.get() | ||||
|             .listOfObjects(Track::class.java) | ||||
|             .withQuery(Query.builder() | ||||
|                     .table(TrackTable.TABLE) | ||||
|                     .where("${TrackTable.COL_MANGA_ID} = ?") | ||||
|                     .whereArgs(manga.id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|  | ||||
|     fun insertTrack(track: Track) = db.put().`object`(track).prepare() | ||||
|  | ||||
|     fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare() | ||||
|  | ||||
|     fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete() | ||||
|             .byQuery(DeleteQuery.builder() | ||||
|                     .table(TrackTable.TABLE) | ||||
|                     .where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?") | ||||
|                     .whereArgs(manga.id, sync.id) | ||||
|                     .build()) | ||||
|             .prepare() | ||||
|  | ||||
| } | ||||
| package eu.kanade.tachiyomi.data.database.queries | ||||
|  | ||||
| import com.pushtorefresh.storio.sqlite.queries.DeleteQuery | ||||
| import com.pushtorefresh.storio.sqlite.queries.Query | ||||
| import eu.kanade.tachiyomi.data.database.DbProvider | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.data.database.tables.TrackTable | ||||
| import eu.kanade.tachiyomi.data.track.TrackService | ||||
|  | ||||
| interface TrackQueries : DbProvider { | ||||
|  | ||||
|     fun getTracks(manga: Manga) = db.get() | ||||
|         .listOfObjects(Track::class.java) | ||||
|         .withQuery( | ||||
|             Query.builder() | ||||
|                 .table(TrackTable.TABLE) | ||||
|                 .where("${TrackTable.COL_MANGA_ID} = ?") | ||||
|                 .whereArgs(manga.id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
|  | ||||
|     fun insertTrack(track: Track) = db.put().`object`(track).prepare() | ||||
|  | ||||
|     fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare() | ||||
|  | ||||
|     fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete() | ||||
|         .byQuery( | ||||
|             DeleteQuery.builder() | ||||
|                 .table(TrackTable.TABLE) | ||||
|                 .where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?") | ||||
|                 .whereArgs(manga.id, sync.id) | ||||
|                 .build() | ||||
|         ) | ||||
|         .prepare() | ||||
| } | ||||
|   | ||||
| @@ -20,16 +20,14 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() | ||||
|             .table(ChapterTable.TABLE) | ||||
|             .where("${ChapterTable.COL_URL} = ?") | ||||
|             .whereArgs(chapter.url) | ||||
|             .build() | ||||
|         .table(ChapterTable.TABLE) | ||||
|         .where("${ChapterTable.COL_URL} = ?") | ||||
|         .whereArgs(chapter.url) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply { | ||||
|         put(ChapterTable.COL_READ, chapter.read) | ||||
|         put(ChapterTable.COL_BOOKMARK, chapter.bookmark) | ||||
|         put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read) | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -20,16 +20,14 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() | ||||
|             .table(ChapterTable.TABLE) | ||||
|             .where("${ChapterTable.COL_ID} = ?") | ||||
|             .whereArgs(chapter.id) | ||||
|             .build() | ||||
|         .table(ChapterTable.TABLE) | ||||
|         .where("${ChapterTable.COL_ID} = ?") | ||||
|         .whereArgs(chapter.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply { | ||||
|         put(ChapterTable.COL_READ, chapter.read) | ||||
|         put(ChapterTable.COL_BOOKMARK, chapter.bookmark) | ||||
|         put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read) | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -20,13 +20,12 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() | ||||
|             .table(ChapterTable.TABLE) | ||||
|             .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") | ||||
|             .whereArgs(chapter.url, chapter.manga_id) | ||||
|             .build() | ||||
|         .table(ChapterTable.TABLE) | ||||
|         .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") | ||||
|         .whereArgs(chapter.url, chapter.manga_id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply { | ||||
|         put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order) | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -19,25 +19,25 @@ class HistoryLastReadPutResolver : HistoryPutResolver() { | ||||
|     override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn { | ||||
|         val updateQuery = mapToUpdateQuery(history) | ||||
|  | ||||
|         val cursor = db.lowLevel().query(Query.builder() | ||||
|         val cursor = db.lowLevel().query( | ||||
|             Query.builder() | ||||
|                 .table(updateQuery.table()) | ||||
|                 .where(updateQuery.where()) | ||||
|                 .whereArgs(updateQuery.whereArgs()) | ||||
|                 .build()) | ||||
|                 .build() | ||||
|         ) | ||||
|  | ||||
|         val putResult: PutResult | ||||
|  | ||||
|         try { | ||||
|             if (cursor.count == 0) { | ||||
|         putResult = cursor.use { putCursor -> | ||||
|             if (putCursor.count == 0) { | ||||
|                 val insertQuery = mapToInsertQuery(history) | ||||
|                 val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history)) | ||||
|                 putResult = PutResult.newInsertResult(insertedId, insertQuery.table()) | ||||
|                 PutResult.newInsertResult(insertedId, insertQuery.table()) | ||||
|             } else { | ||||
|                 val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history)) | ||||
|                 putResult = PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table()) | ||||
|                 PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table()) | ||||
|             } | ||||
|         } finally { | ||||
|             cursor.close() | ||||
|         } | ||||
|  | ||||
|         putResult | ||||
| @@ -48,10 +48,10 @@ class HistoryLastReadPutResolver : HistoryPutResolver() { | ||||
|      * @param obj history object | ||||
|      */ | ||||
|     override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder() | ||||
|             .table(HistoryTable.TABLE) | ||||
|             .where("${HistoryTable.COL_CHAPTER_ID} = ?") | ||||
|             .whereArgs(obj.chapter_id) | ||||
|             .build() | ||||
|         .table(HistoryTable.TABLE) | ||||
|         .where("${HistoryTable.COL_CHAPTER_ID} = ?") | ||||
|         .whereArgs(obj.chapter_id) | ||||
|         .build() | ||||
|  | ||||
|     /** | ||||
|      * Create content query | ||||
| @@ -60,5 +60,4 @@ class HistoryLastReadPutResolver : HistoryPutResolver() { | ||||
|     fun mapToUpdateContentValues(history: History) = ContentValues(1).apply { | ||||
|         put(HistoryTable.COL_LAST_READ, history.last_read) | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -21,5 +21,4 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet | ||||
|  | ||||
|         return manga | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -24,5 +24,4 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() { | ||||
|  | ||||
|         return MangaChapter(manga, chapter) | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -20,14 +20,12 @@ class MangaFavoritePutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_FAVORITE, manga.favorite) | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -20,14 +20,12 @@ class MangaFlagsPutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags) | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -20,14 +20,12 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_LAST_UPDATE, manga.last_update) | ||||
|     } | ||||
|  | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -20,13 +20,12 @@ class MangaTitlePutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_TITLE, manga.title) | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -20,13 +20,12 @@ class MangaViewerPutResolver : PutResolver<Manga>() { | ||||
|     } | ||||
|  | ||||
|     fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() | ||||
|             .table(MangaTable.TABLE) | ||||
|             .where("${MangaTable.COL_ID} = ?") | ||||
|             .whereArgs(manga.id) | ||||
|             .build() | ||||
|         .table(MangaTable.TABLE) | ||||
|         .where("${MangaTable.COL_ID} = ?") | ||||
|         .whereArgs(manga.id) | ||||
|         .build() | ||||
|  | ||||
|     fun mapToContentValues(manga: Manga) = ContentValues(1).apply { | ||||
|         put(MangaTable.COL_VIEWER, manga.viewer) | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -13,11 +13,11 @@ object CategoryTable { | ||||
|     const val COL_FLAGS = "flags" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_NAME TEXT NOT NULL, | ||||
|             $COL_ORDER INTEGER NOT NULL, | ||||
|             $COL_FLAGS INTEGER NOT NULL | ||||
|             )""" | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -29,7 +29,8 @@ object ChapterTable { | ||||
|     const val COL_SOURCE_ORDER = "source_order" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_MANGA_ID INTEGER NOT NULL, | ||||
|             $COL_URL TEXT NOT NULL, | ||||
| @@ -51,7 +52,7 @@ object ChapterTable { | ||||
|  | ||||
|     val createUnreadChaptersIndexQuery: String | ||||
|         get() = "CREATE INDEX ${TABLE}_unread_by_manga_index ON $TABLE($COL_MANGA_ID, $COL_READ) " + | ||||
|                 "WHERE $COL_READ = 0" | ||||
|             "WHERE $COL_READ = 0" | ||||
|  | ||||
|     val sourceOrderUpdateQuery: String | ||||
|         get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0" | ||||
| @@ -61,5 +62,4 @@ object ChapterTable { | ||||
|  | ||||
|     val addScanlator: String | ||||
|         get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL" | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -31,7 +31,8 @@ object HistoryTable { | ||||
|      * query to create history table | ||||
|      */ | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_CHAPTER_ID INTEGER NOT NULL UNIQUE, | ||||
|             $COL_LAST_READ LONG, | ||||
|   | ||||
| @@ -11,7 +11,8 @@ object MangaCategoryTable { | ||||
|     const val COL_CATEGORY_ID = "category_id" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_MANGA_ID INTEGER NOT NULL, | ||||
|             $COL_CATEGORY_ID INTEGER NOT NULL, | ||||
| @@ -20,5 +21,4 @@ object MangaCategoryTable { | ||||
|             FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) | ||||
|             ON DELETE CASCADE | ||||
|             )""" | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -39,7 +39,8 @@ object MangaTable { | ||||
|     const val COL_CATEGORY = "category" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_SOURCE INTEGER NOT NULL, | ||||
|             $COL_URL TEXT NOT NULL, | ||||
| @@ -62,5 +63,5 @@ object MangaTable { | ||||
|  | ||||
|     val createLibraryIndexQuery: String | ||||
|         get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " + | ||||
|                 "WHERE $COL_FAVORITE = 1" | ||||
|             "WHERE $COL_FAVORITE = 1" | ||||
| } | ||||
|   | ||||
| @@ -26,8 +26,13 @@ object TrackTable { | ||||
|  | ||||
|     const val COL_TRACKING_URL = "remote_url" | ||||
|  | ||||
|     const val COL_START_DATE = "start_date" | ||||
|  | ||||
|     const val COL_FINISH_DATE = "finish_date" | ||||
|  | ||||
|     val createTableQuery: String | ||||
|         get() = """CREATE TABLE $TABLE( | ||||
|         get() = | ||||
|             """CREATE TABLE $TABLE( | ||||
|             $COL_ID INTEGER NOT NULL PRIMARY KEY, | ||||
|             $COL_MANGA_ID INTEGER NOT NULL, | ||||
|             $COL_SYNC_ID INTEGER NOT NULL, | ||||
| @@ -39,6 +44,8 @@ object TrackTable { | ||||
|             $COL_STATUS INTEGER NOT NULL, | ||||
|             $COL_SCORE FLOAT NOT NULL, | ||||
|             $COL_TRACKING_URL TEXT NOT NULL, | ||||
|             $COL_START_DATE LONG NOT NULL, | ||||
|             $COL_FINISH_DATE LONG NOT NULL, | ||||
|             UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE, | ||||
|             FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) | ||||
|             ON DELETE CASCADE | ||||
| @@ -49,4 +56,10 @@ object TrackTable { | ||||
|  | ||||
|     val addLibraryId: String | ||||
|         get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL" | ||||
|  | ||||
|     val addStartDate: String | ||||
|         get() = "ALTER TABLE $TABLE ADD COLUMN $COL_START_DATE LONG NOT NULL DEFAULT 0" | ||||
|  | ||||
|     val addFinishDate: String | ||||
|         get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0" | ||||
| } | ||||
|   | ||||
| @@ -6,11 +6,11 @@ import com.hippo.unifile.UniFile | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.preference.getOrDefault | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import java.util.concurrent.TimeUnit | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.concurrent.TimeUnit | ||||
|  | ||||
| /** | ||||
|  * Cache where we dump the downloads directory from the filesystem. This class is needed because | ||||
| @@ -24,10 +24,10 @@ import java.util.concurrent.TimeUnit | ||||
|  * @param preferences the preferences of the app. | ||||
|  */ | ||||
| class DownloadCache( | ||||
|         private val context: Context, | ||||
|         private val provider: DownloadProvider, | ||||
|         private val sourceManager: SourceManager, | ||||
|         private val preferences: PreferencesHelper = Injekt.get() | ||||
|     private val context: Context, | ||||
|     private val provider: DownloadProvider, | ||||
|     private val sourceManager: SourceManager, | ||||
|     private val preferences: PreferencesHelper = Injekt.get() | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
| @@ -47,19 +47,18 @@ class DownloadCache( | ||||
|     private var rootDir = RootDirectory(getDirectoryFromPreference()) | ||||
|  | ||||
|     init { | ||||
|         preferences.downloadsDirectory().asObservable() | ||||
|                 .skip(1) | ||||
|                 .subscribe { | ||||
|                     lastRenew = 0L // invalidate cache | ||||
|                     rootDir = RootDirectory(getDirectoryFromPreference()) | ||||
|                 } | ||||
|         preferences.downloadsDirectory().asFlow() | ||||
|             .onEach { | ||||
|                 lastRenew = 0L // invalidate cache | ||||
|                 rootDir = RootDirectory(getDirectoryFromPreference()) | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns the downloads directory from the user's preferences. | ||||
|      */ | ||||
|     private fun getDirectoryFromPreference(): UniFile { | ||||
|         val dir = preferences.downloadsDirectory().getOrDefault() | ||||
|         val dir = preferences.downloadsDirectory().get() | ||||
|         return UniFile.fromUri(context, Uri.parse(dir)) | ||||
|     } | ||||
|  | ||||
| @@ -100,7 +99,9 @@ class DownloadCache( | ||||
|         if (sourceDir != null) { | ||||
|             val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] | ||||
|             if (mangaDir != null) { | ||||
|                 return mangaDir.files.size | ||||
|                 return mangaDir.files | ||||
|                     .filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) } | ||||
|                     .size | ||||
|             } | ||||
|         } | ||||
|         return 0 | ||||
| @@ -124,26 +125,26 @@ class DownloadCache( | ||||
|         val onlineSources = sourceManager.getOnlineSources() | ||||
|  | ||||
|         val sourceDirs = rootDir.dir.listFiles() | ||||
|                 .orEmpty() | ||||
|                 .associate { it.name to SourceDirectory(it) } | ||||
|                 .mapNotNullKeys { entry -> | ||||
|                     onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id | ||||
|                 } | ||||
|             .orEmpty() | ||||
|             .associate { it.name to SourceDirectory(it) } | ||||
|             .mapNotNullKeys { entry -> | ||||
|                 onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id | ||||
|             } | ||||
|  | ||||
|         rootDir.files = sourceDirs | ||||
|  | ||||
|         sourceDirs.values.forEach { sourceDir -> | ||||
|             val mangaDirs = sourceDir.dir.listFiles() | ||||
|                     .orEmpty() | ||||
|                     .associateNotNullKeys { it.name to MangaDirectory(it) } | ||||
|                 .orEmpty() | ||||
|                 .associateNotNullKeys { it.name to MangaDirectory(it) } | ||||
|  | ||||
|             sourceDir.files = mangaDirs | ||||
|  | ||||
|             mangaDirs.values.forEach { mangaDir -> | ||||
|                 val chapterDirs = mangaDir.dir.listFiles() | ||||
|                         .orEmpty() | ||||
|                         .mapNotNull { it.name } | ||||
|                         .toHashSet() | ||||
|                     .orEmpty() | ||||
|                     .mapNotNull { it.name } | ||||
|                     .toHashSet() | ||||
|  | ||||
|                 mangaDir.files = chapterDirs | ||||
|             } | ||||
| @@ -231,27 +232,33 @@ class DownloadCache( | ||||
|     /** | ||||
|      * Class to store the files under the root downloads directory. | ||||
|      */ | ||||
|     private class RootDirectory(val dir: UniFile, | ||||
|                                 var files: Map<Long, SourceDirectory> = hashMapOf()) | ||||
|     private class RootDirectory( | ||||
|         val dir: UniFile, | ||||
|         var files: Map<Long, SourceDirectory> = hashMapOf() | ||||
|     ) | ||||
|  | ||||
|     /** | ||||
|      * Class to store the files under a source directory. | ||||
|      */ | ||||
|     private class SourceDirectory(val dir: UniFile, | ||||
|                                   var files: Map<String, MangaDirectory> = hashMapOf()) | ||||
|     private class SourceDirectory( | ||||
|         val dir: UniFile, | ||||
|         var files: Map<String, MangaDirectory> = hashMapOf() | ||||
|     ) | ||||
|  | ||||
|     /** | ||||
|      * Class to store the files under a manga directory. | ||||
|      */ | ||||
|     private class MangaDirectory(val dir: UniFile, | ||||
|                                  var files: Set<String> = hashSetOf()) | ||||
|     private class MangaDirectory( | ||||
|         val dir: UniFile, | ||||
|         var files: Set<String> = hashSetOf() | ||||
|     ) | ||||
|  | ||||
|     /** | ||||
|      * Returns a new map containing only the key entries of [transform] that are not null. | ||||
|      */ | ||||
|     private inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): Map<R, V> { | ||||
|         val destination = LinkedHashMap<R, V>() | ||||
|         forEach { element -> transform(element)?.let { destination.put(it, element.value) } } | ||||
|         forEach { element -> transform(element)?.let { destination[it] = element.value } } | ||||
|         return destination | ||||
|     } | ||||
|  | ||||
| @@ -263,10 +270,9 @@ class DownloadCache( | ||||
|         for (element in this) { | ||||
|             val (key, value) = transform(element) | ||||
|             if (key != null) { | ||||
|                 destination.put(key, value) | ||||
|                 destination[key] = value | ||||
|             } | ||||
|         } | ||||
|         return destination | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -20,7 +20,7 @@ import uy.kohesive.injekt.injectLazy | ||||
|  * | ||||
|  * @param context the application context. | ||||
|  */ | ||||
| class DownloadManager(context: Context) { | ||||
| class DownloadManager(private val context: Context) { | ||||
|  | ||||
|     /** | ||||
|      * The sources manager. | ||||
| @@ -99,10 +99,21 @@ class DownloadManager(context: Context) { | ||||
|      * @param downloads value to set the download queue to | ||||
|      */ | ||||
|     fun reorderQueue(downloads: List<Download>) { | ||||
|         val wasRunning = downloader.isRunning | ||||
|  | ||||
|         if (downloads.isEmpty()) { | ||||
|             DownloadService.stop(context) | ||||
|             downloader.queue.clear() | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         downloader.pause() | ||||
|         downloader.queue.clear() | ||||
|         downloader.queue.addAll(downloads) | ||||
|         downloader.start() | ||||
|  | ||||
|         if (wasRunning) { | ||||
|             downloader.start() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -137,16 +148,16 @@ class DownloadManager(context: Context) { | ||||
|     private fun buildPageList(chapterDir: UniFile?): Observable<List<Page>> { | ||||
|         return Observable.fromCallable { | ||||
|             val files = chapterDir?.listFiles().orEmpty() | ||||
|                     .filter { "image" in it.type.orEmpty() } | ||||
|                 .filter { "image" in it.type.orEmpty() } | ||||
|  | ||||
|             if (files.isEmpty()) { | ||||
|                 throw Exception("Page list is empty") | ||||
|             } | ||||
|  | ||||
|             files.sortedBy { it.name } | ||||
|                     .mapIndexed { i, file -> | ||||
|                         Page(i, uri = file.uri).apply { status = Page.READY } | ||||
|                     } | ||||
|                 .mapIndexed { i, file -> | ||||
|                     Page(i, uri = file.uri).apply { status = Page.READY } | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -170,6 +181,15 @@ class DownloadManager(context: Context) { | ||||
|         return cache.getDownloadCount(manga) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Calls delete chapter, which deletes a temp download. | ||||
|      * | ||||
|      * @param download the download to cancel. | ||||
|      */ | ||||
|     fun deletePendingDownload(download: Download) { | ||||
|         deleteChapters(listOf(download.chapter), download.manga, download.source) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Deletes the directories of a list of downloaded chapters. | ||||
|      * | ||||
| @@ -219,5 +239,4 @@ class DownloadManager(context: Context) { | ||||
|             deleteChapters(chapters, manga, source) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -5,13 +5,16 @@ import android.graphics.BitmapFactory | ||||
| import androidx.core.app.NotificationCompat | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.download.model.Download | ||||
| import eu.kanade.tachiyomi.data.download.model.DownloadQueue | ||||
| import eu.kanade.tachiyomi.data.notification.NotificationHandler | ||||
| import eu.kanade.tachiyomi.data.notification.NotificationReceiver | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.util.chop | ||||
| import eu.kanade.tachiyomi.util.notificationManager | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.util.lang.chop | ||||
| import eu.kanade.tachiyomi.util.system.notificationBuilder | ||||
| import eu.kanade.tachiyomi.util.system.notificationManager | ||||
| import java.util.regex.Pattern | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| /** | ||||
|  * DownloadNotifier is used to show notifications when downloading one or multiple chapters. | ||||
| @@ -19,40 +22,23 @@ import java.util.regex.Pattern | ||||
|  * @param context context of application | ||||
|  */ | ||||
| internal class DownloadNotifier(private val context: Context) { | ||||
|     /** | ||||
|      * Notification builder. | ||||
|      */ | ||||
|     private val notification by lazy { | ||||
|         NotificationCompat.Builder(context, Notifications.CHANNEL_DOWNLOADER) | ||||
|                 .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) | ||||
|  | ||||
|     private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) { | ||||
|         setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) | ||||
|     } | ||||
|  | ||||
|     private val preferences by lazy { Injekt.get<PreferencesHelper>() } | ||||
|  | ||||
|     /** | ||||
|      * Status of download. Used for correct notification icon. | ||||
|      */ | ||||
|     private var isDownloading = false | ||||
|  | ||||
|     /** | ||||
|      * The size of queue on start download. | ||||
|      */ | ||||
|     var initialQueueSize = 0 | ||||
|         set(value) { | ||||
|             if (value != 0) { | ||||
|                 isSingleChapter = (value == 1) | ||||
|             } | ||||
|             field = value | ||||
|         } | ||||
|  | ||||
|     /** | ||||
|      * Updated when error is thrown | ||||
|      */ | ||||
|     var errorThrown = false | ||||
|  | ||||
|     /** | ||||
|      * Updated when only single page is downloaded | ||||
|      */ | ||||
|     var isSingleChapter = false | ||||
|  | ||||
|     /** | ||||
|      * Updated when paused | ||||
|      */ | ||||
| @@ -70,9 +56,10 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|     /** | ||||
|      * Clear old actions if they exist. | ||||
|      */ | ||||
|     private fun clearActions() = with(notification) { | ||||
|         if (!mActions.isEmpty()) | ||||
|     private fun clearActions() = with(notificationBuilder) { | ||||
|         if (mActions.isNotEmpty()) { | ||||
|             mActions.clear() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -90,7 +77,7 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|      */ | ||||
|     fun onProgressChange(download: Download) { | ||||
|         // Create notification | ||||
|         with(notification) { | ||||
|         with(notificationBuilder) { | ||||
|             // Check if first call. | ||||
|             if (!isDownloading) { | ||||
|                 setSmallIcon(android.R.drawable.stat_sys_download) | ||||
| @@ -100,84 +87,65 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|                 setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) | ||||
|                 isDownloading = true | ||||
|                 // Pause action | ||||
|                 addAction(R.drawable.ic_av_pause_grey_24dp_img, | ||||
|                         context.getString(R.string.action_pause), | ||||
|                         NotificationReceiver.pauseDownloadsPendingBroadcast(context)) | ||||
|                 addAction( | ||||
|                     R.drawable.ic_pause_24dp, | ||||
|                     context.getString(R.string.action_pause), | ||||
|                     NotificationReceiver.pauseDownloadsPendingBroadcast(context) | ||||
|                 ) | ||||
|             } | ||||
|  | ||||
|             val downloadingProgressText = context.getString(R.string.chapter_downloading_progress) | ||||
|                 .format(download.downloadedImages, download.pages!!.size) | ||||
|  | ||||
|             if (preferences.hideNotificationContent()) { | ||||
|                 setContentTitle(downloadingProgressText) | ||||
|             } else { | ||||
|                 val title = download.manga.title.chop(15) | ||||
|                 val quotedTitle = Pattern.quote(title) | ||||
|                 val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "") | ||||
|                 setContentTitle("$title - $chapter".chop(30)) | ||||
|                 setContentText(downloadingProgressText) | ||||
|             } | ||||
|  | ||||
|             val title = download.manga.title.chop(15) | ||||
|             val quotedTitle = Pattern.quote(title) | ||||
|             val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "") | ||||
|             setContentTitle("$title - $chapter".chop(30)) | ||||
|             setContentText(context.getString(R.string.chapter_downloading_progress) | ||||
|                     .format(download.downloadedImages, download.pages!!.size)) | ||||
|             setProgress(download.pages!!.size, download.downloadedImages, false) | ||||
|         } | ||||
|  | ||||
|         // Displays the progress bar on notification | ||||
|         notification.show() | ||||
|         notificationBuilder.show() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Show notification when download is paused. | ||||
|      */ | ||||
|     fun onDownloadPaused() { | ||||
|         with(notification) { | ||||
|         with(notificationBuilder) { | ||||
|             setContentTitle(context.getString(R.string.chapter_paused)) | ||||
|             setContentText(context.getString(R.string.download_notifier_download_paused)) | ||||
|             setSmallIcon(R.drawable.ic_av_pause_grey_24dp_img) | ||||
|             setSmallIcon(R.drawable.ic_pause_24dp) | ||||
|             setAutoCancel(false) | ||||
|             setProgress(0, 0, false) | ||||
|             clearActions() | ||||
|             // Open download manager when clicked | ||||
|             setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) | ||||
|             // Resume action | ||||
|             addAction(R.drawable.ic_av_play_arrow_grey_img, | ||||
|                     context.getString(R.string.action_resume), | ||||
|                     NotificationReceiver.resumeDownloadsPendingBroadcast(context)) | ||||
|             //Clear action | ||||
|             addAction(R.drawable.ic_clear_grey_24dp_img, | ||||
|                     context.getString(R.string.action_clear), | ||||
|                     NotificationReceiver.clearDownloadsPendingBroadcast(context)) | ||||
|             addAction( | ||||
|                 R.drawable.ic_play_arrow_24dp, | ||||
|                 context.getString(R.string.action_resume), | ||||
|                 NotificationReceiver.resumeDownloadsPendingBroadcast(context) | ||||
|             ) | ||||
|             // Clear action | ||||
|             addAction( | ||||
|                 R.drawable.ic_close_24dp, | ||||
|                 context.getString(R.string.action_cancel_all), | ||||
|                 NotificationReceiver.clearDownloadsPendingBroadcast(context) | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         // Show notification. | ||||
|         notification.show() | ||||
|         notificationBuilder.show() | ||||
|  | ||||
|         // Reset initial values | ||||
|         isDownloading = false | ||||
|         initialQueueSize = 0 | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Called when chapter is downloaded. | ||||
|      * | ||||
|      * @param download download object containing download information. | ||||
|      */ | ||||
|     fun onDownloadCompleted(download: Download, queue: DownloadQueue) { | ||||
|         // Check if last download | ||||
|         if (!queue.isEmpty()) { | ||||
|             return | ||||
|         } | ||||
|         // Create notification. | ||||
|         with(notification) { | ||||
|             val title = download.manga.title.chop(15) | ||||
|             val quotedTitle = Pattern.quote(title) | ||||
|             val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "") | ||||
|             setContentTitle("$title - $chapter".chop(30)) | ||||
|             setContentText(context.getString(R.string.update_check_notification_download_complete)) | ||||
|             setSmallIcon(android.R.drawable.stat_sys_download_done) | ||||
|             setAutoCancel(true) | ||||
|             clearActions() | ||||
|             setContentIntent(NotificationReceiver.openChapterPendingBroadcast(context, download.manga, download.chapter)) | ||||
|             setProgress(0, 0, false) | ||||
|         } | ||||
|  | ||||
|         // Show notification. | ||||
|         notification.show() | ||||
|  | ||||
|         // Reset initial values | ||||
|         isDownloading = false | ||||
|         initialQueueSize = 0 | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -186,7 +154,7 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|      * @param reason the text to show. | ||||
|      */ | ||||
|     fun onWarning(reason: String) { | ||||
|         with(notification) { | ||||
|         with(notificationBuilder) { | ||||
|             setContentTitle(context.getString(R.string.download_notifier_downloader_title)) | ||||
|             setContentText(reason) | ||||
|             setSmallIcon(android.R.drawable.stat_sys_warning) | ||||
| @@ -195,7 +163,7 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|             setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) | ||||
|             setProgress(0, 0, false) | ||||
|         } | ||||
|         notification.show() | ||||
|         notificationBuilder.show() | ||||
|  | ||||
|         // Reset download information | ||||
|         isDownloading = false | ||||
| @@ -210,16 +178,19 @@ internal class DownloadNotifier(private val context: Context) { | ||||
|      */ | ||||
|     fun onError(error: String? = null, chapter: String? = null) { | ||||
|         // Create notification | ||||
|         with(notification) { | ||||
|             setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title)) | ||||
|             setContentText(error ?: context.getString(R.string.download_notifier_unkown_error)) | ||||
|         with(notificationBuilder) { | ||||
|             setContentTitle( | ||||
|                 chapter | ||||
|                     ?: context.getString(R.string.download_notifier_downloader_title) | ||||
|             ) | ||||
|             setContentText(error ?: context.getString(R.string.download_notifier_unknown_error)) | ||||
|             setSmallIcon(android.R.drawable.stat_sys_warning) | ||||
|             clearActions() | ||||
|             setAutoCancel(false) | ||||
|             setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) | ||||
|             setProgress(0, 0, false) | ||||
|         } | ||||
|         notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR) | ||||
|         notificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR) | ||||
|  | ||||
|         // Reset download information | ||||
|         errorThrown = true | ||||
|   | ||||
| @@ -120,27 +120,27 @@ class DownloadPendingDeleter(context: Context) { | ||||
|      * Class used to save an entry of chapters with their manga into preferences. | ||||
|      */ | ||||
|     private data class Entry( | ||||
|             val chapters: List<ChapterEntry>, | ||||
|             val manga: MangaEntry | ||||
|         val chapters: List<ChapterEntry>, | ||||
|         val manga: MangaEntry | ||||
|     ) | ||||
|  | ||||
|     /** | ||||
|      * Class used to save an entry for a chapter into preferences. | ||||
|      */ | ||||
|     private data class ChapterEntry( | ||||
|             val id: Long, | ||||
|             val url: String, | ||||
|             val name: String | ||||
|         val id: Long, | ||||
|         val url: String, | ||||
|         val name: String | ||||
|     ) | ||||
|  | ||||
|     /** | ||||
|      * Class used to save an entry for a manga into preferences. | ||||
|      */ | ||||
|     private data class MangaEntry( | ||||
|             val id: Long, | ||||
|             val url: String, | ||||
|             val title: String, | ||||
|             val source: Long | ||||
|         val id: Long, | ||||
|         val url: String, | ||||
|         val title: String, | ||||
|         val source: Long | ||||
|     ) | ||||
|  | ||||
|     /** | ||||
| @@ -176,5 +176,4 @@ class DownloadPendingDeleter(context: Context) { | ||||
|             it.name = name | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -3,12 +3,17 @@ package eu.kanade.tachiyomi.data.download | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| import com.hippo.unifile.UniFile | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.preference.getOrDefault | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.util.DiskUtil | ||||
| import eu.kanade.tachiyomi.util.storage.DiskUtil | ||||
| import kotlinx.coroutines.CoroutineScope | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.Job | ||||
| import kotlinx.coroutines.flow.launchIn | ||||
| import kotlinx.coroutines.flow.onEach | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| /** | ||||
| @@ -19,24 +24,23 @@ import uy.kohesive.injekt.injectLazy | ||||
|  */ | ||||
| class DownloadProvider(private val context: Context) { | ||||
|  | ||||
|     /** | ||||
|      * Preferences helper. | ||||
|      */ | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
|  | ||||
|     private val scope = CoroutineScope(Job() + Dispatchers.Main) | ||||
|  | ||||
|     /** | ||||
|      * The root directory for downloads. | ||||
|      */ | ||||
|     private var downloadsDir = preferences.downloadsDirectory().getOrDefault().let { | ||||
|     private var downloadsDir = preferences.downloadsDirectory().get().let { | ||||
|         val dir = UniFile.fromUri(context, Uri.parse(it)) | ||||
|         DiskUtil.createNoMediaFile(dir, context) | ||||
|         dir | ||||
|     } | ||||
|  | ||||
|     init { | ||||
|         preferences.downloadsDirectory().asObservable() | ||||
|                 .skip(1) | ||||
|                 .subscribe { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) } | ||||
|         preferences.downloadsDirectory().asFlow() | ||||
|             .onEach { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) } | ||||
|             .launchIn(scope) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -46,9 +50,13 @@ class DownloadProvider(private val context: Context) { | ||||
|      * @param source the source of the manga. | ||||
|      */ | ||||
|     internal fun getMangaDir(manga: Manga, source: Source): UniFile { | ||||
|         return downloadsDir | ||||
|         try { | ||||
|             return downloadsDir | ||||
|                 .createDirectory(getSourceDirName(source)) | ||||
|                 .createDirectory(getMangaDirName(manga)) | ||||
|         } catch (e: NullPointerException) { | ||||
|             throw Exception(context.getString(R.string.invalid_download_dir)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -121,5 +129,4 @@ class DownloadProvider(private val context: Context) { | ||||
|     fun getChapterDirName(chapter: Chapter): String { | ||||
|         return DiskUtil.buildValidFilename(chapter.name) | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -9,17 +9,17 @@ import android.net.NetworkInfo.State.DISCONNECTED | ||||
| import android.os.Build | ||||
| import android.os.IBinder | ||||
| import android.os.PowerManager | ||||
| import androidx.core.app.NotificationCompat | ||||
| import com.github.pwittchen.reactivenetwork.library.Connectivity | ||||
| import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork | ||||
| import com.jakewharton.rxrelay.BehaviorRelay | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.util.connectivityManager | ||||
| import eu.kanade.tachiyomi.util.plusAssign | ||||
| import eu.kanade.tachiyomi.util.powerManager | ||||
| import eu.kanade.tachiyomi.util.toast | ||||
| import eu.kanade.tachiyomi.util.lang.plusAssign | ||||
| import eu.kanade.tachiyomi.util.system.connectivityManager | ||||
| import eu.kanade.tachiyomi.util.system.notification | ||||
| import eu.kanade.tachiyomi.util.system.powerManager | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import rx.android.schedulers.AndroidSchedulers | ||||
| import rx.schedulers.Schedulers | ||||
| import rx.subscriptions.CompositeSubscription | ||||
| @@ -63,14 +63,8 @@ class DownloadService : Service() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Download manager. | ||||
|      */ | ||||
|     private val downloadManager: DownloadManager by injectLazy() | ||||
|  | ||||
|     /** | ||||
|      * Preferences helper. | ||||
|      */ | ||||
|     private val preferences: PreferencesHelper by injectLazy() | ||||
|  | ||||
|     /** | ||||
| @@ -112,7 +106,7 @@ class DownloadService : Service() { | ||||
|      * Not used. | ||||
|      */ | ||||
|     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | ||||
|         return Service.START_NOT_STICKY | ||||
|         return START_NOT_STICKY | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -129,13 +123,17 @@ class DownloadService : Service() { | ||||
|      */ | ||||
|     private fun listenNetworkChanges() { | ||||
|         subscriptions += ReactiveNetwork.observeNetworkConnectivity(applicationContext) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe({ state -> onNetworkStateChanged(state) | ||||
|                 }, { | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe( | ||||
|                 { state -> | ||||
|                     onNetworkStateChanged(state) | ||||
|                 }, | ||||
|                 { | ||||
|                     toast(R.string.download_queue_error) | ||||
|                     stopSelf() | ||||
|                 }) | ||||
|                 } | ||||
|             ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -156,7 +154,9 @@ class DownloadService : Service() { | ||||
|             DISCONNECTED -> { | ||||
|                 downloadManager.stopDownloads(getString(R.string.download_notifier_no_network)) | ||||
|             } | ||||
|             else -> { /* Do nothing */ } | ||||
|             else -> { | ||||
|                 /* Do nothing */ | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -165,10 +165,11 @@ class DownloadService : Service() { | ||||
|      */ | ||||
|     private fun listenDownloaderState() { | ||||
|         subscriptions += downloadManager.runningRelay.subscribe { running -> | ||||
|             if (running) | ||||
|             if (running) { | ||||
|                 wakeLock.acquireIfNeeded() | ||||
|             else | ||||
|             } else { | ||||
|                 wakeLock.releaseIfNeeded() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -187,9 +188,8 @@ class DownloadService : Service() { | ||||
|     } | ||||
|  | ||||
|     private fun getPlaceholderNotification(): Notification { | ||||
|         return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER) | ||||
|             .setContentTitle(getString(R.string.download_notifier_downloader_title)) | ||||
|             .build() | ||||
|         return notification(Notifications.CHANNEL_DOWNLOADER) { | ||||
|             setContentTitle(getString(R.string.download_notifier_downloader_title)) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -15,8 +15,8 @@ import uy.kohesive.injekt.injectLazy | ||||
|  * @param context the application context. | ||||
|  */ | ||||
| class DownloadStore( | ||||
|         context: Context, | ||||
|         private val sourceManager: SourceManager | ||||
|     context: Context, | ||||
|     private val sourceManager: SourceManager | ||||
| ) { | ||||
|  | ||||
|     /** | ||||
| @@ -29,9 +29,6 @@ class DownloadStore( | ||||
|      */ | ||||
|     private val gson: Gson by injectLazy() | ||||
|  | ||||
|     /** | ||||
|      * Database helper. | ||||
|      */ | ||||
|     private val db: DatabaseHelper by injectLazy() | ||||
|  | ||||
|     /** | ||||
| @@ -80,9 +77,9 @@ class DownloadStore( | ||||
|      */ | ||||
|     fun restore(): List<Download> { | ||||
|         val objs = preferences.all | ||||
|                 .mapNotNull { it.value as? String } | ||||
|                 .mapNotNull { deserialize(it) } | ||||
|                 .sortedBy { it.order } | ||||
|             .mapNotNull { it.value as? String } | ||||
|             .mapNotNull { deserialize(it) } | ||||
|             .sortedBy { it.order } | ||||
|  | ||||
|         val downloads = mutableListOf<Download>() | ||||
|         if (objs.isNotEmpty()) { | ||||
| @@ -133,5 +130,4 @@ class DownloadStore( | ||||
|      * @param order the order of the download in the queue. | ||||
|      */ | ||||
|     data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int) | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import com.elvishew.xlog.XLog | ||||
| import com.hippo.unifile.UniFile | ||||
| import com.jakewharton.rxrelay.BehaviorRelay | ||||
| import com.jakewharton.rxrelay.PublishRelay | ||||
| import eu.kanade.tachiyomi.data.cache.ChapterCache | ||||
| import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.download.model.Download | ||||
| @@ -14,7 +15,14 @@ import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList | ||||
| import eu.kanade.tachiyomi.util.* | ||||
| import eu.kanade.tachiyomi.util.lang.RetryWithDelay | ||||
| import eu.kanade.tachiyomi.util.lang.launchNow | ||||
| import eu.kanade.tachiyomi.util.lang.launchUI | ||||
| import eu.kanade.tachiyomi.util.lang.plusAssign | ||||
| import eu.kanade.tachiyomi.util.storage.DiskUtil | ||||
| import eu.kanade.tachiyomi.util.storage.saveTo | ||||
| import eu.kanade.tachiyomi.util.system.ImageUtil | ||||
| import java.io.File | ||||
| import kotlinx.coroutines.async | ||||
| import okhttp3.Response | ||||
| import rx.Observable | ||||
| @@ -22,6 +30,7 @@ import rx.android.schedulers.AndroidSchedulers | ||||
| import rx.schedulers.Schedulers | ||||
| import rx.subscriptions.CompositeSubscription | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| /** | ||||
|  * This class is the one in charge of downloading chapters. | ||||
| @@ -38,12 +47,14 @@ import timber.log.Timber | ||||
|  * @param sourceManager the source manager. | ||||
|  */ | ||||
| class Downloader( | ||||
|         private val context: Context, | ||||
|         private val provider: DownloadProvider, | ||||
|         private val cache: DownloadCache, | ||||
|         private val sourceManager: SourceManager | ||||
|     private val context: Context, | ||||
|     private val provider: DownloadProvider, | ||||
|     private val cache: DownloadCache, | ||||
|     private val sourceManager: SourceManager | ||||
| ) { | ||||
|  | ||||
|     private val chapterCache: ChapterCache by injectLazy() | ||||
|  | ||||
|     /** | ||||
|      * Store for persisting downloads across restarts. | ||||
|      */ | ||||
| @@ -77,7 +88,9 @@ class Downloader( | ||||
|     /** | ||||
|      * Whether the downloader is running. | ||||
|      */ | ||||
|     @Volatile private var isRunning: Boolean = false | ||||
|     @Volatile | ||||
|     var isRunning: Boolean = false | ||||
|         private set | ||||
|  | ||||
|     init { | ||||
|         launchNow { | ||||
| @@ -93,17 +106,19 @@ class Downloader( | ||||
|      * @return true if the downloader is started, false otherwise. | ||||
|      */ | ||||
|     fun start(): Boolean { | ||||
|         if (isRunning || queue.isEmpty()) | ||||
|         if (isRunning || queue.isEmpty()) { | ||||
|             return false | ||||
|         notifier.paused = false | ||||
|         if (!subscriptions.hasSubscriptions()) | ||||
|         } | ||||
|  | ||||
|         if (!subscriptions.hasSubscriptions()) { | ||||
|             initializeSubscriptions() | ||||
|         } | ||||
|  | ||||
|         val pending = queue.filter { it.status != Download.DOWNLOADED } | ||||
|         pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE } | ||||
|  | ||||
|         downloadsRelay.call(pending) | ||||
|         return !pending.isEmpty() | ||||
|         return pending.isNotEmpty() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -112,8 +127,8 @@ class Downloader( | ||||
|     fun stop(reason: String? = null) { | ||||
|         destroySubscriptions() | ||||
|         queue | ||||
|                 .filter { it.status == Download.DOWNLOADING } | ||||
|                 .forEach { it.status = Download.ERROR } | ||||
|             .filter { it.status == Download.DOWNLOADING } | ||||
|             .forEach { it.status = Download.ERROR } | ||||
|  | ||||
|         if (reason != null) { | ||||
|             notifier.onWarning(reason) | ||||
| @@ -121,8 +136,6 @@ class Downloader( | ||||
|             if (notifier.paused) { | ||||
|                 notifier.paused = false | ||||
|                 notifier.onDownloadPaused() | ||||
|             } else if (notifier.isSingleChapter && !notifier.errorThrown) { | ||||
|                 notifier.isSingleChapter = false | ||||
|             } else { | ||||
|                 notifier.dismiss() | ||||
|             } | ||||
| @@ -135,8 +148,8 @@ class Downloader( | ||||
|     fun pause() { | ||||
|         destroySubscriptions() | ||||
|         queue | ||||
|                 .filter { it.status == Download.DOWNLOADING } | ||||
|                 .forEach { it.status = Download.QUEUE } | ||||
|             .filter { it.status == Download.DOWNLOADING } | ||||
|             .forEach { it.status = Download.QUEUE } | ||||
|         notifier.paused = true | ||||
|     } | ||||
|  | ||||
| @@ -148,11 +161,11 @@ class Downloader( | ||||
|     fun clearQueue(isNotification: Boolean = false) { | ||||
|         destroySubscriptions() | ||||
|  | ||||
|         //Needed to update the chapter view | ||||
|         // Needed to update the chapter view | ||||
|         if (isNotification) { | ||||
|             queue | ||||
|                     .filter { it.status == Download.QUEUE } | ||||
|                     .forEach { it.status = Download.NOT_DOWNLOADED } | ||||
|                 .filter { it.status == Download.QUEUE } | ||||
|                 .forEach { it.status = Download.NOT_DOWNLOADED } | ||||
|         } | ||||
|         queue.clear() | ||||
|         notifier.dismiss() | ||||
| @@ -169,15 +182,19 @@ class Downloader( | ||||
|         subscriptions.clear() | ||||
|  | ||||
|         subscriptions += downloadsRelay.concatMapIterable { it } | ||||
|                 .concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) } | ||||
|                 .onBackpressureBuffer() | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe({ completeDownload(it) | ||||
|                 }, { error -> | ||||
|             .concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) } | ||||
|             .onBackpressureBuffer() | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe( | ||||
|                 { | ||||
|                     completeDownload(it) | ||||
|                 }, | ||||
|                 { error -> | ||||
|                     DownloadService.stop(context) | ||||
|                     Timber.e(error) | ||||
|                     notifier.onError(error.message) | ||||
|                 }) | ||||
|                 } | ||||
|             ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -200,40 +217,37 @@ class Downloader( | ||||
|      */ | ||||
|     fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI { | ||||
|         val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI | ||||
|  | ||||
|         val wasEmpty = queue.isEmpty() | ||||
|         // Called in background thread, the operation can be slow with SAF. | ||||
|         val chaptersWithoutDir = async { | ||||
|             val mangaDir = provider.findMangaDir(manga, source) | ||||
|  | ||||
|             chapters | ||||
|                     // Avoid downloading chapters with the same name. | ||||
|                     .distinctBy { it.name } | ||||
|                     // Filter out those already downloaded. | ||||
|                     .filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null } | ||||
|                     // Add chapters to queue from the start. | ||||
|                     .sortedByDescending { it.source_order } | ||||
|                 // Avoid downloading chapters with the same name. | ||||
|                 .distinctBy { it.name } | ||||
|                 // Filter out those already downloaded. | ||||
|                 .filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null } | ||||
|                 // Add chapters to queue from the start. | ||||
|                 .sortedByDescending { it.source_order } | ||||
|         } | ||||
|  | ||||
|         // Runs in main thread (synchronization needed). | ||||
|         val chaptersToQueue = chaptersWithoutDir.await() | ||||
|                 // Filter out those already enqueued. | ||||
|                 .filter { chapter -> queue.none { it.chapter.id == chapter.id } } | ||||
|                 // Create a download for each one. | ||||
|                 .map { Download(source, manga, it) } | ||||
|             // Filter out those already enqueued. | ||||
|             .filter { chapter -> queue.none { it.chapter.id == chapter.id } } | ||||
|             // Create a download for each one. | ||||
|             .map { Download(source, manga, it) } | ||||
|  | ||||
|         if (chaptersToQueue.isNotEmpty()) { | ||||
|             queue.addAll(chaptersToQueue) | ||||
|  | ||||
|             // Initialize queue size. | ||||
|             notifier.initialQueueSize = queue.size | ||||
|  | ||||
|             if (isRunning) { | ||||
|                 // Send the list of downloads to the downloader. | ||||
|                 downloadsRelay.call(chaptersToQueue) | ||||
|             } | ||||
|  | ||||
|             // Start downloader if needed | ||||
|             if (autoStart) { | ||||
|             if (autoStart && wasEmpty) { | ||||
|                 DownloadService.start(this@Downloader.context) | ||||
|             } | ||||
|         } | ||||
| @@ -247,59 +261,48 @@ class Downloader( | ||||
|     private fun downloadChapter(download: Download): Observable<Download> = Observable.defer { | ||||
|         val chapterDirname = provider.getChapterDirName(download.chapter) | ||||
|         val mangaDir = provider.getMangaDir(download.manga, download.source) | ||||
|         val tmpDir = mangaDir.createDirectory("${chapterDirname}_tmp") | ||||
|         val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX) | ||||
|  | ||||
|         val pageListObservable = if (download.pages == null) { | ||||
|             // Pull page list from network and add them to download object | ||||
|             download.source.fetchPageList(download.chapter) | ||||
|                     .doOnNext { pages -> | ||||
|                         if (pages.isEmpty()) { | ||||
|                             throw Exception("Page list is empty") | ||||
|                         } | ||||
|                         download.pages = pages | ||||
|                 .doOnNext { pages -> | ||||
|                     if (pages.isEmpty()) { | ||||
|                         throw Exception("Page list is empty") | ||||
|                     } | ||||
|                     download.pages = pages | ||||
|                 } | ||||
|         } else { | ||||
|             // Or if the page list already exists, start from the file | ||||
|             Observable.just(download.pages!!) | ||||
|         } | ||||
|  | ||||
|         pageListObservable | ||||
|                 .doOnNext { _ -> | ||||
|                     // Delete all temporary (unfinished) files | ||||
|                     tmpDir.listFiles() | ||||
|                             ?.filter { it.name!!.endsWith(".tmp") } | ||||
|                             ?.forEach { it.delete() } | ||||
|  | ||||
|                     download.downloadedImages = 0 | ||||
|                     download.status = Download.DOWNLOADING | ||||
|                 } | ||||
|                 // Get all the URLs to the source images, fetch pages if necessary | ||||
|                 .flatMap { download.source.fetchAllImageUrlsFromPageList(it) } | ||||
|                 // Start downloading images, consider we can have downloaded images already | ||||
|                 .concatMap { page -> getOrDownloadImage(page, download, tmpDir) } | ||||
|                 // Do when page is downloaded. | ||||
|                 .doOnNext { notifier.onProgressChange(download) } | ||||
|                 .toList() | ||||
|                 .map { _ -> download } | ||||
|                 // Do after download completes | ||||
|                 .doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) } | ||||
|                 // If the page list threw, it will resume here | ||||
|                 .onErrorReturn { error -> | ||||
|                     // [EXH] | ||||
|                     XLog.w("> Download error!", error) | ||||
|                     XLog.w("> (source.id: %s, source.name: %s, manga.id: %s, manga.url: %s, chapter.id: %s, chapter.url: %s)", | ||||
|                             download.source.id, | ||||
|                             download.source.name, | ||||
|                             download.manga.id, | ||||
|                             download.manga.url, | ||||
|                             download.chapter.id, | ||||
|                             download.chapter.url) | ||||
|  | ||||
|                     download.status = Download.ERROR | ||||
|                     notifier.onError(error.message, download.chapter.name) | ||||
|                     download | ||||
|                 } | ||||
|             .doOnNext { _ -> | ||||
|                 // Delete all temporary (unfinished) files | ||||
|                 tmpDir.listFiles() | ||||
|                     ?.filter { it.name!!.endsWith(".tmp") } | ||||
|                     ?.forEach { it.delete() } | ||||
|  | ||||
|                 download.downloadedImages = 0 | ||||
|                 download.status = Download.DOWNLOADING | ||||
|             } | ||||
|             // Get all the URLs to the source images, fetch pages if necessary | ||||
|             .flatMap { download.source.fetchAllImageUrlsFromPageList(it) } | ||||
|             // Start downloading images, consider we can have downloaded images already | ||||
|             .concatMap { page -> getOrDownloadImage(page, download, tmpDir) } | ||||
|             // Do when page is downloaded. | ||||
|             .doOnNext { notifier.onProgressChange(download) } | ||||
|             .toList() | ||||
|             .map { download } | ||||
|             // Do after download completes | ||||
|             .doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) } | ||||
|             // If the page list threw, it will resume here | ||||
|             .onErrorReturn { error -> | ||||
|                 download.status = Download.ERROR | ||||
|                 notifier.onError(error.message, download.chapter.name) | ||||
|                 download | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -312,8 +315,9 @@ class Downloader( | ||||
|      */ | ||||
|     private fun getOrDownloadImage(page: Page, download: Download, tmpDir: UniFile): Observable<Page> { | ||||
|         // If the image URL is empty, do nothing | ||||
|         if (page.imageUrl == null) | ||||
|         if (page.imageUrl == null) { | ||||
|             return Observable.just(page) | ||||
|         } | ||||
|  | ||||
|         val filename = String.format("%03d", page.number) | ||||
|         val tmpFile = tmpDir.findFile("$filename.tmp") | ||||
| @@ -325,26 +329,27 @@ class Downloader( | ||||
|         val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") } | ||||
|  | ||||
|         // If the image is already downloaded, do nothing. Otherwise download from network | ||||
|         val pageObservable = if (imageFile != null) | ||||
|             Observable.just(imageFile) | ||||
|         else | ||||
|             downloadImage(page, download.source, tmpDir, filename) | ||||
|         val pageObservable = when { | ||||
|             imageFile != null -> Observable.just(imageFile) | ||||
|             chapterCache.isImageInCache(page.imageUrl!!) -> copyImageFromCache(chapterCache.getImageFile(page.imageUrl!!), tmpDir, filename) | ||||
|             else -> downloadImage(page, download.source, tmpDir, filename) | ||||
|         } | ||||
|  | ||||
|         return pageObservable | ||||
|                 // When the image is ready, set image path, progress (just in case) and status | ||||
|                 .doOnNext { file -> | ||||
|                     page.uri = file.uri | ||||
|                     page.progress = 100 | ||||
|                     download.downloadedImages++ | ||||
|                     page.status = Page.READY | ||||
|                 } | ||||
|                 .map { page } | ||||
|                 // Mark this page as error and allow to download the remaining | ||||
|                 .onErrorReturn { | ||||
|                     page.progress = 0 | ||||
|                     page.status = Page.ERROR | ||||
|                     page | ||||
|                 } | ||||
|             // When the image is ready, set image path, progress (just in case) and status | ||||
|             .doOnNext { file -> | ||||
|                 page.uri = file.uri | ||||
|                 page.progress = 100 | ||||
|                 download.downloadedImages++ | ||||
|                 page.status = Page.READY | ||||
|             } | ||||
|             .map { page } | ||||
|             // Mark this page as error and allow to download the remaining | ||||
|             .onErrorReturn { | ||||
|                 page.progress = 0 | ||||
|                 page.status = Page.ERROR | ||||
|                 page | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -359,30 +364,43 @@ class Downloader( | ||||
|         page.status = Page.DOWNLOAD_IMAGE | ||||
|         page.progress = 0 | ||||
|         return source.fetchImage(page) | ||||
|                 .map { response -> | ||||
|                     val file = tmpDir.createFile("$filename.tmp") | ||||
|                     try { | ||||
|                         response.body!!.source().saveTo(file.openOutputStream()) | ||||
|                         val extension = getImageExtension(response, file) | ||||
|                         file.renameTo("$filename.$extension") | ||||
|                     } catch (e: Exception) { | ||||
|                         // [EXH] | ||||
|                         XLog.w("> Failed to fetch image!", e) | ||||
|                         XLog.w("> (source.id: %s, source.name: %s, page.index: %s, page.url: %s, page.imageUrl: %s)", | ||||
|                                 source.id, | ||||
|                                 source.name, | ||||
|                                 page.index, | ||||
|                                 page.url, | ||||
|                                 page.imageUrl) | ||||
|  | ||||
|                         response.close() | ||||
|                         file.delete() | ||||
|                         throw e | ||||
|                     } | ||||
|                     file | ||||
|             .map { response -> | ||||
|                 val file = tmpDir.createFile("$filename.tmp") | ||||
|                 try { | ||||
|                     response.body!!.source().saveTo(file.openOutputStream()) | ||||
|                     val extension = getImageExtension(response, file) | ||||
|                     file.renameTo("$filename.$extension") | ||||
|                 } catch (e: Exception) { | ||||
|                     response.close() | ||||
|                     file.delete() | ||||
|                     throw e | ||||
|                 } | ||||
|                 // Retry 3 times, waiting 2, 4 and 8 seconds between attempts. | ||||
|                 .retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline())) | ||||
|                 file | ||||
|             } | ||||
|             // Retry 3 times, waiting 2, 4 and 8 seconds between attempts. | ||||
|             .retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline())) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Return the observable which copies the image from cache. | ||||
|      * | ||||
|      * @param cacheFile the file from cache. | ||||
|      * @param tmpDir the temporary directory of the download. | ||||
|      * @param filename the filename of the image. | ||||
|      */ | ||||
|     private fun copyImageFromCache(cacheFile: File, tmpDir: UniFile, filename: String): Observable<UniFile> { | ||||
|         return Observable.just(cacheFile).map { | ||||
|             val tmpFile = tmpDir.createFile("$filename.tmp") | ||||
|             cacheFile.inputStream().use { input -> | ||||
|                 tmpFile.openOutputStream().use { output -> | ||||
|                     input.copyTo(output) | ||||
|                 } | ||||
|             } | ||||
|             val extension = ImageUtil.findImageType(cacheFile.inputStream()) ?: return@map tmpFile | ||||
|             tmpFile.renameTo("$filename.${extension.extension}") | ||||
|             cacheFile.delete() | ||||
|             tmpFile | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -411,9 +429,12 @@ class Downloader( | ||||
|      * @param tmpDir the directory where the download is currently stored. | ||||
|      * @param dirname the real (non temporary) directory name of the download. | ||||
|      */ | ||||
|     private fun ensureSuccessfulDownload(download: Download, mangaDir: UniFile, | ||||
|                                          tmpDir: UniFile, dirname: String) { | ||||
|  | ||||
|     private fun ensureSuccessfulDownload( | ||||
|         download: Download, | ||||
|         mangaDir: UniFile, | ||||
|         tmpDir: UniFile, | ||||
|         dirname: String | ||||
|     ) { | ||||
|         // Ensure that the chapter folder has all the images. | ||||
|         val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") } | ||||
|  | ||||
| @@ -442,9 +463,6 @@ class Downloader( | ||||
|             queue.remove(download) | ||||
|         } | ||||
|         if (areAllDownloadsFinished()) { | ||||
|             if (notifier.isSingleChapter && !notifier.errorThrown) { | ||||
|                 notifier.onDownloadCompleted(download, queue) | ||||
|             } | ||||
|             DownloadService.stop(context) | ||||
|         } | ||||
|     } | ||||
| @@ -456,4 +474,7 @@ class Downloader( | ||||
|         return queue.none { it.status <= Download.DOWNLOADING } | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         const val TMP_DIR_SUFFIX = "_tmp" | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -10,28 +10,48 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) { | ||||
|  | ||||
|     var pages: List<Page>? = null | ||||
|  | ||||
|     @Volatile @Transient var totalProgress: Int = 0 | ||||
|     @Volatile | ||||
|     @Transient | ||||
|     var totalProgress: Int = 0 | ||||
|  | ||||
|     @Volatile @Transient var downloadedImages: Int = 0 | ||||
|     @Volatile | ||||
|     @Transient | ||||
|     var downloadedImages: Int = 0 | ||||
|  | ||||
|     @Volatile @Transient var status: Int = 0 | ||||
|     @Volatile | ||||
|     @Transient | ||||
|     var status: Int = 0 | ||||
|         set(status) { | ||||
|             field = status | ||||
|             statusSubject?.onNext(this) | ||||
|             statusCallback?.invoke(this) | ||||
|         } | ||||
|  | ||||
|     @Transient private var statusSubject: PublishSubject<Download>? = null | ||||
|     @Transient | ||||
|     private var statusSubject: PublishSubject<Download>? = null | ||||
|  | ||||
|     @Transient | ||||
|     private var statusCallback: ((Download) -> Unit)? = null | ||||
|  | ||||
|     val progress: Int | ||||
|         get() { | ||||
|             val pages = pages ?: return 0 | ||||
|             return pages.map(Page::progress).average().toInt() | ||||
|         } | ||||
|  | ||||
|     fun setStatusSubject(subject: PublishSubject<Download>?) { | ||||
|         statusSubject = subject | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|     fun setStatusCallback(f: ((Download) -> Unit)?) { | ||||
|         statusCallback = f | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         const val NOT_DOWNLOADED = 0 | ||||
|         const val QUEUE = 1 | ||||
|         const val DOWNLOADING = 2 | ||||
|         const val DOWNLOADED = 3 | ||||
|         const val ERROR = 4 | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -5,14 +5,14 @@ import eu.kanade.tachiyomi.data.database.models.Chapter | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.download.DownloadStore | ||||
| import eu.kanade.tachiyomi.source.model.Page | ||||
| import java.util.concurrent.CopyOnWriteArrayList | ||||
| import rx.Observable | ||||
| import rx.subjects.PublishSubject | ||||
| import java.util.concurrent.CopyOnWriteArrayList | ||||
|  | ||||
| class DownloadQueue( | ||||
|         private val store: DownloadStore, | ||||
|         private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>()) | ||||
| : List<Download> by queue { | ||||
|     private val store: DownloadStore, | ||||
|     private val queue: MutableList<Download> = CopyOnWriteArrayList() | ||||
| ) : List<Download> by queue { | ||||
|  | ||||
|     private val statusSubject = PublishSubject.create<Download>() | ||||
|  | ||||
| @@ -21,6 +21,7 @@ class DownloadQueue( | ||||
|     fun addAll(downloads: List<Download>) { | ||||
|         downloads.forEach { download -> | ||||
|             download.setStatusSubject(statusSubject) | ||||
|             download.setStatusCallback(::setPagesFor) | ||||
|             download.status = Download.QUEUE | ||||
|         } | ||||
|         queue.addAll(downloads) | ||||
| @@ -32,6 +33,10 @@ class DownloadQueue( | ||||
|         val removed = queue.remove(download) | ||||
|         store.remove(download) | ||||
|         download.setStatusSubject(null) | ||||
|         download.setStatusCallback(null) | ||||
|         if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) { | ||||
|             download.status = Download.NOT_DOWNLOADED | ||||
|         } | ||||
|         if (removed) { | ||||
|             updatedRelay.call(Unit) | ||||
|         } | ||||
| @@ -42,7 +47,9 @@ class DownloadQueue( | ||||
|     } | ||||
|  | ||||
|     fun remove(chapters: List<Chapter>) { | ||||
|         for (chapter in chapters) { remove(chapter) } | ||||
|         for (chapter in chapters) { | ||||
|             remove(chapter) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun remove(manga: Manga) { | ||||
| @@ -52,6 +59,10 @@ class DownloadQueue( | ||||
|     fun clear() { | ||||
|         queue.forEach { download -> | ||||
|             download.setStatusSubject(null) | ||||
|             download.setStatusCallback(null) | ||||
|             if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) { | ||||
|                 download.status = Download.NOT_DOWNLOADED | ||||
|             } | ||||
|         } | ||||
|         queue.clear() | ||||
|         store.clear() | ||||
| @@ -64,35 +75,35 @@ class DownloadQueue( | ||||
|     fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer() | ||||
|  | ||||
|     fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer() | ||||
|             .startWith(Unit) | ||||
|             .map { this } | ||||
|         .startWith(Unit) | ||||
|         .map { this } | ||||
|  | ||||
|     fun getProgressObservable(): Observable<Download> { | ||||
|         return statusSubject.onBackpressureBuffer() | ||||
|                 .startWith(getActiveDownloads()) | ||||
|                 .flatMap { download -> | ||||
|                     if (download.status == Download.DOWNLOADING) { | ||||
|                         val pageStatusSubject = PublishSubject.create<Int>() | ||||
|                         setPagesSubject(download.pages, pageStatusSubject) | ||||
|                         return@flatMap pageStatusSubject | ||||
|                                 .onBackpressureBuffer() | ||||
|                                 .filter { it == Page.READY } | ||||
|                                 .map { download } | ||||
|  | ||||
|                     } else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { | ||||
|                         setPagesSubject(download.pages, null) | ||||
|                     } | ||||
|                     Observable.just(download) | ||||
|                 } | ||||
|                 .filter { it.status == Download.DOWNLOADING } | ||||
|     } | ||||
|  | ||||
|     private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) { | ||||
|         if (pages != null) { | ||||
|             for (page in pages) { | ||||
|                 page.setStatusSubject(subject) | ||||
|             } | ||||
|     private fun setPagesFor(download: Download) { | ||||
|         if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { | ||||
|             setPagesSubject(download.pages, null) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fun getProgressObservable(): Observable<Download> { | ||||
|         return statusSubject.onBackpressureBuffer() | ||||
|             .startWith(getActiveDownloads()) | ||||
|             .flatMap { download -> | ||||
|                 if (download.status == Download.DOWNLOADING) { | ||||
|                     val pageStatusSubject = PublishSubject.create<Int>() | ||||
|                     setPagesSubject(download.pages, pageStatusSubject) | ||||
|                     return@flatMap pageStatusSubject | ||||
|                         .onBackpressureBuffer() | ||||
|                         .filter { it == Page.READY } | ||||
|                         .map { download } | ||||
|                 } else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) { | ||||
|                     setPagesSubject(download.pages, null) | ||||
|                 } | ||||
|                 Observable.just(download) | ||||
|             } | ||||
|             .filter { it.status == Download.DOWNLOADING } | ||||
|     } | ||||
|  | ||||
|     private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) { | ||||
|         pages?.forEach { it.setStatusSubject(subject) } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -5,7 +5,12 @@ import android.util.Log | ||||
| import com.bumptech.glide.Priority | ||||
| import com.bumptech.glide.load.DataSource | ||||
| import com.bumptech.glide.load.data.DataFetcher | ||||
| import java.io.* | ||||
| import java.io.File | ||||
| import java.io.FileInputStream | ||||
| import java.io.FileNotFoundException | ||||
| import java.io.IOException | ||||
| import java.io.InputStream | ||||
| import timber.log.Timber | ||||
|  | ||||
| open class FileFetcher(private val file: File) : DataFetcher<InputStream> { | ||||
|  | ||||
| @@ -20,7 +25,7 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> { | ||||
|             data = FileInputStream(file) | ||||
|         } catch (e: FileNotFoundException) { | ||||
|             if (Log.isLoggable(TAG, Log.DEBUG)) { | ||||
|                 Log.d(TAG, "Failed to open file", e) | ||||
|                 Timber.d(e, "Failed to open file") | ||||
|             } | ||||
|             callback.onLoadFailed(e) | ||||
|             return | ||||
| @@ -48,4 +53,4 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> { | ||||
|     override fun getDataSource(): DataSource { | ||||
|         return DataSource.LOCAL | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -16,44 +16,48 @@ import java.io.InputStream | ||||
|  * @param manga the manga of the cover to load. | ||||
|  * @param file the file where this cover should be. It may exists or not. | ||||
|  */ | ||||
| class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>, | ||||
|                              private val manga: Manga, | ||||
|                              private val file: File) | ||||
| : FileFetcher(file) { | ||||
| class LibraryMangaUrlFetcher( | ||||
|     private val networkFetcher: DataFetcher<InputStream>, | ||||
|     private val manga: Manga, | ||||
|     private val file: File | ||||
| ) : | ||||
|     FileFetcher(file) { | ||||
|  | ||||
|     override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) { | ||||
|         if (!file.exists()) { | ||||
|             networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> { | ||||
|                 override fun onDataReady(data: InputStream?) { | ||||
|                     if (data != null) { | ||||
|                         val tmpFile = File(file.path + ".tmp") | ||||
|                         try { | ||||
|                             // Retrieve destination stream, create parent folders if needed. | ||||
|                             val output = try { | ||||
|                                 tmpFile.outputStream() | ||||
|                             } catch (e: FileNotFoundException) { | ||||
|                                 tmpFile.parentFile.mkdirs() | ||||
|                                 tmpFile.outputStream() | ||||
|                             } | ||||
|             networkFetcher.loadData( | ||||
|                 priority, | ||||
|                 object : DataFetcher.DataCallback<InputStream> { | ||||
|                     override fun onDataReady(data: InputStream?) { | ||||
|                         if (data != null) { | ||||
|                             val tmpFile = File(file.path + ".tmp") | ||||
|                             try { | ||||
|                                 // Retrieve destination stream, create parent folders if needed. | ||||
|                                 val output = try { | ||||
|                                     tmpFile.outputStream() | ||||
|                                 } catch (e: FileNotFoundException) { | ||||
|                                     tmpFile.parentFile.mkdirs() | ||||
|                                     tmpFile.outputStream() | ||||
|                                 } | ||||
|  | ||||
|                             // Copy the file and rename to the original. | ||||
|                             data.use { output.use { data.copyTo(output) } } | ||||
|                             tmpFile.renameTo(file) | ||||
|                             loadFromFile(callback) | ||||
|                         } catch (e: Exception) { | ||||
|                             tmpFile.delete() | ||||
|                             callback.onLoadFailed(e) | ||||
|                                 // Copy the file and rename to the original. | ||||
|                                 data.use { output.use { data.copyTo(output) } } | ||||
|                                 tmpFile.renameTo(file) | ||||
|                                 loadFromFile(callback) | ||||
|                             } catch (e: Exception) { | ||||
|                                 tmpFile.delete() | ||||
|                                 callback.onLoadFailed(e) | ||||
|                             } | ||||
|                         } else { | ||||
|                             callback.onLoadFailed(Exception("Null data")) | ||||
|                         } | ||||
|                     } else { | ||||
|                         callback.onLoadFailed(Exception("Null data")) | ||||
|                     } | ||||
|  | ||||
|                     override fun onLoadFailed(e: Exception) { | ||||
|                         callback.onLoadFailed(e) | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 override fun onLoadFailed(e: Exception) { | ||||
|                     callback.onLoadFailed(e) | ||||
|                 } | ||||
|  | ||||
|             }) | ||||
|             ) | ||||
|         } else { | ||||
|             loadFromFile(callback) | ||||
|         } | ||||
| @@ -68,5 +72,4 @@ class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream | ||||
|         super.cancel() | ||||
|         networkFetcher.cancel() | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -24,4 +24,4 @@ class MangaSignature(manga: Manga, file: File) : Key { | ||||
|     override fun updateDiskCacheKey(md: MessageDigest) { | ||||
|         md.update(key.toByteArray(Key.CHARSET)) | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,7 @@ | ||||
| package eu.kanade.tachiyomi.data.glide | ||||
|  | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
|  | ||||
| data class MangaThumbnail(val manga: Manga, val url: String?) | ||||
|  | ||||
| fun Manga.toMangaThumbnail() = MangaThumbnail(this, this.thumbnail_url) | ||||
| @@ -3,18 +3,22 @@ package eu.kanade.tachiyomi.data.glide | ||||
| import android.util.LruCache | ||||
| import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher | ||||
| import com.bumptech.glide.load.Options | ||||
| import com.bumptech.glide.load.model.* | ||||
| import com.bumptech.glide.load.model.GlideUrl | ||||
| import com.bumptech.glide.load.model.Headers | ||||
| import com.bumptech.glide.load.model.LazyHeaders | ||||
| import com.bumptech.glide.load.model.ModelLoader | ||||
| import com.bumptech.glide.load.model.ModelLoaderFactory | ||||
| import com.bumptech.glide.load.model.MultiModelLoaderFactory | ||||
| import eu.kanade.tachiyomi.data.cache.CoverCache | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.network.NetworkHelper | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import java.io.File | ||||
| import java.io.InputStream | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import java.io.File | ||||
| import java.io.InputStream | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * A class for loading a cover associated with a [Manga] that can be present in our own cache. | ||||
| @@ -27,7 +31,7 @@ import java.io.InputStream | ||||
|  * | ||||
|  * @param context the application context. | ||||
|  */ | ||||
| class MangaModelLoader : ModelLoader<Manga, InputStream> { | ||||
| class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> { | ||||
| 
 | ||||
|     /** | ||||
|      * Cover cache where persistent covers are stored. | ||||
| @@ -56,18 +60,18 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> { | ||||
|     private val cachedHeaders = hashMapOf<Long, LazyHeaders>() | ||||
| 
 | ||||
|     /** | ||||
|      * Factory class for creating [MangaModelLoader] instances. | ||||
|      * Factory class for creating [MangaThumbnailModelLoader] instances. | ||||
|      */ | ||||
|     class Factory : ModelLoaderFactory<Manga, InputStream> { | ||||
|     class Factory : ModelLoaderFactory<MangaThumbnail, InputStream> { | ||||
| 
 | ||||
|         override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<Manga, InputStream> { | ||||
|             return MangaModelLoader() | ||||
|         override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MangaThumbnail, InputStream> { | ||||
|             return MangaThumbnailModelLoader() | ||||
|         } | ||||
| 
 | ||||
|         override fun teardown() {} | ||||
|     } | ||||
| 
 | ||||
|     override fun handles(model: Manga): Boolean { | ||||
|     override fun handles(model: MangaThumbnail): Boolean { | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
| @@ -78,15 +82,21 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> { | ||||
|      * @param width the width of the view where the resource will be loaded. | ||||
|      * @param height the height of the view where the resource will be loaded. | ||||
|      */ | ||||
|     override fun buildLoadData(manga: Manga, width: Int, height: Int, | ||||
|                                options: Options): ModelLoader.LoadData<InputStream>? { | ||||
|     override fun buildLoadData( | ||||
|         mangaThumbnail: MangaThumbnail, | ||||
|         width: Int, | ||||
|         height: Int, | ||||
|         options: Options | ||||
|     ): ModelLoader.LoadData<InputStream>? { | ||||
|         // Check thumbnail is not null or empty | ||||
|         val url = manga.thumbnail_url | ||||
|         val url = mangaThumbnail.url | ||||
|         if (url == null || url.isEmpty()) { | ||||
|             return null | ||||
|         } | ||||
| 
 | ||||
|         if (url.startsWith("http")) { | ||||
|         val manga = mangaThumbnail.manga | ||||
| 
 | ||||
|         if (url.startsWith("http", true)) { | ||||
|             val source = sourceManager.get(manga.source) as? HttpSource | ||||
|             val glideUrl = GlideUrl(url, getHeaders(manga, source)) | ||||
| 
 | ||||
| @@ -118,7 +128,7 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> { | ||||
|      * | ||||
|      * @param manga the model. | ||||
|      */ | ||||
|     fun getHeaders(manga: Manga, source: HttpSource?): Headers { | ||||
|     private fun getHeaders(manga: Manga, source: HttpSource?): Headers { | ||||
|         if (source == null) return LazyHeaders.DEFAULT | ||||
| 
 | ||||
|         return cachedHeaders.getOrPut(manga.source) { | ||||
| @@ -142,5 +152,4 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> { | ||||
|             value | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @@ -14,10 +14,10 @@ import java.io.InputStream | ||||
| class PassthroughModelLoader : ModelLoader<InputStream, InputStream> { | ||||
|  | ||||
|     override fun buildLoadData( | ||||
|             model: InputStream, | ||||
|             width: Int, | ||||
|             height: Int, | ||||
|             options: Options | ||||
|         model: InputStream, | ||||
|         width: Int, | ||||
|         height: Int, | ||||
|         options: Options | ||||
|     ): ModelLoader.LoadData<InputStream>? { | ||||
|         return ModelLoader.LoadData(ObjectKey(model), Fetcher(model)) | ||||
|     } | ||||
| @@ -49,12 +49,11 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> { | ||||
|         } | ||||
|  | ||||
|         override fun loadData( | ||||
|                 priority: Priority, | ||||
|                 callback: DataFetcher.DataCallback<in InputStream> | ||||
|             priority: Priority, | ||||
|             callback: DataFetcher.DataCallback<in InputStream> | ||||
|         ) { | ||||
|             callback.onDataReady(stream) | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -63,12 +62,11 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> { | ||||
|     class Factory : ModelLoaderFactory<InputStream, InputStream> { | ||||
|  | ||||
|         override fun build( | ||||
|                 multiFactory: MultiModelLoaderFactory | ||||
|             multiFactory: MultiModelLoaderFactory | ||||
|         ): ModelLoader<InputStream, InputStream> { | ||||
|             return PassthroughModelLoader() | ||||
|         } | ||||
|  | ||||
|         override fun teardown() {} | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -13,11 +13,10 @@ import com.bumptech.glide.load.model.GlideUrl | ||||
| import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions | ||||
| import com.bumptech.glide.module.AppGlideModule | ||||
| import com.bumptech.glide.request.RequestOptions | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.network.NetworkHelper | ||||
| import java.io.InputStream | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.io.InputStream | ||||
|  | ||||
| /** | ||||
|  * Class used to update Glide module settings | ||||
| @@ -28,16 +27,21 @@ class TachiGlideModule : AppGlideModule() { | ||||
|     override fun applyOptions(context: Context, builder: GlideBuilder) { | ||||
|         builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024)) | ||||
|         builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565)) | ||||
|         builder.setDefaultTransitionOptions(Drawable::class.java, | ||||
|                 DrawableTransitionOptions.withCrossFade()) | ||||
|         builder.setDefaultTransitionOptions( | ||||
|             Drawable::class.java, | ||||
|             DrawableTransitionOptions.withCrossFade() | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     override fun registerComponents(context: Context, glide: Glide, registry: Registry) { | ||||
|         val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client) | ||||
|  | ||||
|         registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory) | ||||
|         registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory()) | ||||
|         registry.append(InputStream::class.java, InputStream::class.java, PassthroughModelLoader | ||||
|             .Factory()) | ||||
|         registry.append(MangaThumbnail::class.java, InputStream::class.java, MangaThumbnailModelLoader.Factory()) | ||||
|         registry.append( | ||||
|             InputStream::class.java, InputStream::class.java, | ||||
|             PassthroughModelLoader | ||||
|                 .Factory() | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,47 +1,58 @@ | ||||
| package eu.kanade.tachiyomi.data.library | ||||
|  | ||||
| import com.evernote.android.job.Job | ||||
| import com.evernote.android.job.JobManager | ||||
| import com.evernote.android.job.JobRequest | ||||
| import android.content.Context | ||||
| import androidx.work.Constraints | ||||
| import androidx.work.ExistingPeriodicWorkPolicy | ||||
| import androidx.work.NetworkType | ||||
| import androidx.work.PeriodicWorkRequestBuilder | ||||
| import androidx.work.WorkManager | ||||
| import androidx.work.Worker | ||||
| import androidx.work.WorkerParameters | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.preference.getOrDefault | ||||
| import java.util.concurrent.TimeUnit | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class LibraryUpdateJob : Job() { | ||||
| class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) : | ||||
|     Worker(context, workerParams) { | ||||
|  | ||||
|     override fun onRunJob(params: Params): Result { | ||||
|     override fun doWork(): Result { | ||||
|         LibraryUpdateService.start(context) | ||||
|         return Job.Result.SUCCESS | ||||
|         return Result.success() | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         const val TAG = "LibraryUpdate" | ||||
|         private const val TAG = "LibraryUpdate" | ||||
|  | ||||
|         fun setupTask(prefInterval: Int? = null) { | ||||
|         fun setupTask(context: Context, prefInterval: Int? = null) { | ||||
|             val preferences = Injekt.get<PreferencesHelper>() | ||||
|             val interval = prefInterval ?: preferences.libraryUpdateInterval().getOrDefault() | ||||
|             val interval = prefInterval ?: preferences.libraryUpdateInterval().get() | ||||
|             if (interval > 0) { | ||||
|                 val restrictions = preferences.libraryUpdateRestriction() | ||||
|                 val restrictions = preferences.libraryUpdateRestriction()!! | ||||
|                 val acRestriction = "ac" in restrictions | ||||
|                 val wifiRestriction = if ("wifi" in restrictions) | ||||
|                     JobRequest.NetworkType.UNMETERED | ||||
|                 else | ||||
|                     JobRequest.NetworkType.CONNECTED | ||||
|                 val wifiRestriction = if ("wifi" in restrictions) { | ||||
|                     NetworkType.UNMETERED | ||||
|                 } else { | ||||
|                     NetworkType.CONNECTED | ||||
|                 } | ||||
|  | ||||
|                 JobRequest.Builder(TAG) | ||||
|                         .setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000) | ||||
|                         .setRequiredNetworkType(wifiRestriction) | ||||
|                         .setRequiresCharging(acRestriction) | ||||
|                         .setRequirementsEnforced(true) | ||||
|                         .setUpdateCurrent(true) | ||||
|                         .build() | ||||
|                         .schedule() | ||||
|                 val constraints = Constraints.Builder() | ||||
|                     .setRequiredNetworkType(wifiRestriction) | ||||
|                     .setRequiresCharging(acRestriction) | ||||
|                     .build() | ||||
|  | ||||
|                 val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>( | ||||
|                     interval.toLong(), TimeUnit.HOURS, | ||||
|                     10, TimeUnit.MINUTES | ||||
|                 ) | ||||
|                     .addTag(TAG) | ||||
|                     .setConstraints(constraints) | ||||
|                     .build() | ||||
|  | ||||
|                 WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request) | ||||
|             } else { | ||||
|                 WorkManager.getInstance(context).cancelAllWorkByTag(TAG) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         fun cancelTask() { | ||||
|             JobManager.instance().cancelAllForTag(TAG) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -8,8 +8,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| object LibraryUpdateRanker { | ||||
|  | ||||
|     val rankingScheme = listOf( | ||||
|             (this::lexicographicRanking)(), | ||||
|             (this::latestFirstRanking)()) | ||||
|         (this::lexicographicRanking)(), | ||||
|         (this::latestFirstRanking)() | ||||
|     ) | ||||
|  | ||||
|     /** | ||||
|      * Provides a total ordering over all the Mangas. | ||||
| @@ -22,7 +23,7 @@ object LibraryUpdateRanker { | ||||
|      */ | ||||
|     fun latestFirstRanking(): Comparator<Manga> { | ||||
|         return Comparator { mangaFirst: Manga, | ||||
|                             mangaSecond: Manga -> | ||||
|             mangaSecond: Manga -> | ||||
|             compareValues(mangaSecond.last_update, mangaFirst.last_update) | ||||
|         } | ||||
|     } | ||||
| @@ -35,9 +36,8 @@ object LibraryUpdateRanker { | ||||
|      */ | ||||
|     fun lexicographicRanking(): Comparator<Manga> { | ||||
|         return Comparator { mangaFirst: Manga, | ||||
|                                    mangaSecond: Manga -> | ||||
|             mangaSecond: Manga -> | ||||
|             compareValues(mangaFirst.title, mangaSecond.title) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,19 @@ | ||||
| package eu.kanade.tachiyomi.data.library | ||||
|  | ||||
| import android.app.Notification | ||||
| import android.app.PendingIntent | ||||
| import android.app.Service | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.graphics.Bitmap | ||||
| import android.graphics.BitmapFactory | ||||
| import android.os.Build | ||||
| import android.os.IBinder | ||||
| import android.os.PowerManager | ||||
| import androidx.core.app.NotificationCompat | ||||
| import androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY | ||||
| import androidx.core.app.NotificationManagerCompat | ||||
| import com.bumptech.glide.Glide | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.database.DatabaseHelper | ||||
| import eu.kanade.tachiyomi.data.database.models.Category | ||||
| @@ -15,28 +22,34 @@ import eu.kanade.tachiyomi.data.database.models.LibraryManga | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.download.DownloadManager | ||||
| import eu.kanade.tachiyomi.data.download.DownloadService | ||||
| import eu.kanade.tachiyomi.data.glide.toMangaThumbnail | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme | ||||
| import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start | ||||
| import eu.kanade.tachiyomi.data.notification.NotificationReceiver | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.data.preference.PreferencesHelper | ||||
| import eu.kanade.tachiyomi.data.preference.getOrDefault | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.source.model.SManga | ||||
| import eu.kanade.tachiyomi.source.online.HttpSource | ||||
| import eu.kanade.tachiyomi.util.isServiceRunning | ||||
| import eu.kanade.tachiyomi.util.notificationManager | ||||
| import eu.kanade.tachiyomi.util.syncChaptersWithSource | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource | ||||
| import eu.kanade.tachiyomi.util.lang.chop | ||||
| import eu.kanade.tachiyomi.util.system.isServiceRunning | ||||
| import eu.kanade.tachiyomi.util.system.notification | ||||
| import eu.kanade.tachiyomi.util.system.notificationBuilder | ||||
| import eu.kanade.tachiyomi.util.system.notificationManager | ||||
| import exh.LIBRARY_UPDATE_EXCLUDED_SOURCES | ||||
| import java.text.DecimalFormat | ||||
| import java.text.DecimalFormatSymbols | ||||
| import java.util.ArrayList | ||||
| import java.util.concurrent.atomic.AtomicInteger | ||||
| import rx.Observable | ||||
| import rx.Subscription | ||||
| import rx.schedulers.Schedulers | ||||
| import timber.log.Timber | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.* | ||||
| import java.util.concurrent.atomic.AtomicInteger | ||||
|  | ||||
| /** | ||||
|  * This class will take care of updating the chapters of the manga from the library. It can be | ||||
| @@ -47,11 +60,11 @@ import java.util.concurrent.atomic.AtomicInteger | ||||
|  * destroyed. | ||||
|  */ | ||||
| class LibraryUpdateService( | ||||
|         val db: DatabaseHelper = Injekt.get(), | ||||
|         val sourceManager: SourceManager = Injekt.get(), | ||||
|         val preferences: PreferencesHelper = Injekt.get(), | ||||
|         val downloadManager: DownloadManager = Injekt.get(), | ||||
|         val trackManager: TrackManager = Injekt.get() | ||||
|     val db: DatabaseHelper = Injekt.get(), | ||||
|     val sourceManager: SourceManager = Injekt.get(), | ||||
|     val preferences: PreferencesHelper = Injekt.get(), | ||||
|     val downloadManager: DownloadManager = Injekt.get(), | ||||
|     val trackManager: TrackManager = Injekt.get() | ||||
| ) : Service() { | ||||
|  | ||||
|     /** | ||||
| @@ -76,13 +89,15 @@ class LibraryUpdateService( | ||||
|     /** | ||||
|      * Cached progress notification to avoid creating a lot. | ||||
|      */ | ||||
|     private val progressNotification by lazy { NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY) | ||||
|             .setContentTitle(getString(R.string.app_name)) | ||||
|             .setSmallIcon(R.drawable.ic_refresh_white_24dp_img) | ||||
|             .setLargeIcon(updateNotifier.notificationBitmap) | ||||
|             .setOngoing(true) | ||||
|             .setOnlyAlertOnce(true) | ||||
|             .addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent) | ||||
|     private val progressNotificationBuilder by lazy { | ||||
|         notificationBuilder(Notifications.CHANNEL_LIBRARY) { | ||||
|             setContentTitle(getString(R.string.app_name)) | ||||
|             setSmallIcon(R.drawable.ic_refresh_24dp) | ||||
|             setLargeIcon(notificationBitmap) | ||||
|             setOngoing(true) | ||||
|             setOnlyAlertOnce(true) | ||||
|             addAction(R.drawable.ic_close_24dp, getString(android.R.string.cancel), cancelIntent) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -90,8 +105,8 @@ class LibraryUpdateService( | ||||
|      */ | ||||
|     enum class Target { | ||||
|         CHAPTERS, // Manga chapters | ||||
|         DETAILS,  // Manga metadata | ||||
|         TRACKING  // Tracking metadata | ||||
|         DETAILS, // Manga metadata | ||||
|         TRACKING // Tracking metadata | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
| @@ -106,6 +121,10 @@ class LibraryUpdateService( | ||||
|          */ | ||||
|         const val KEY_TARGET = "target" | ||||
|  | ||||
|         private const val NOTIF_MAX_CHAPTERS = 5 | ||||
|         private const val NOTIF_TITLE_MAX_LEN = 45 | ||||
|         private const val NOTIF_ICON_SIZE = 192 | ||||
|  | ||||
|         /** | ||||
|          * Returns the status of the service. | ||||
|          * | ||||
| @@ -123,8 +142,9 @@ class LibraryUpdateService( | ||||
|          * @param context the application context. | ||||
|          * @param category a specific category to update, or null for global update. | ||||
|          * @param target defines what should be updated. | ||||
|          * @return true if service newly started, false otherwise | ||||
|          */ | ||||
|         fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS) { | ||||
|         fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS): Boolean { | ||||
|             if (!isRunning(context)) { | ||||
|                 val intent = Intent(context, LibraryUpdateService::class.java).apply { | ||||
|                     putExtra(KEY_TARGET, target) | ||||
| @@ -135,7 +155,11 @@ class LibraryUpdateService( | ||||
|                 } else { | ||||
|                     context.startForegroundService(intent) | ||||
|                 } | ||||
|  | ||||
|                 return true | ||||
|             } | ||||
|  | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         /** | ||||
| @@ -146,7 +170,6 @@ class LibraryUpdateService( | ||||
|         fun stop(context: Context) { | ||||
|             context.stopService(Intent(context, LibraryUpdateService::class.java)) | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -155,9 +178,10 @@ class LibraryUpdateService( | ||||
|      */ | ||||
|     override fun onCreate() { | ||||
|         super.onCreate() | ||||
|         startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build()) | ||||
|         startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotificationBuilder.build()) | ||||
|         wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( | ||||
|                 PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock") | ||||
|             PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock" | ||||
|         ) | ||||
|         wakeLock.acquire() | ||||
|     } | ||||
|  | ||||
| @@ -189,37 +213,41 @@ class LibraryUpdateService( | ||||
|      * @return the start value of the command. | ||||
|      */ | ||||
|     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | ||||
|         if (intent == null) return Service.START_NOT_STICKY | ||||
|         if (intent == null) return START_NOT_STICKY | ||||
|         val target = intent.getSerializableExtra(KEY_TARGET) as? Target | ||||
|                 ?: return Service.START_NOT_STICKY | ||||
|             ?: return START_NOT_STICKY | ||||
|  | ||||
|         // Unsubscribe from any previous subscription if needed. | ||||
|         subscription?.unsubscribe() | ||||
|  | ||||
|         // Update favorite manga. Destroy service when completed or in case of an error. | ||||
|         subscription = Observable | ||||
|                 .defer { | ||||
|                     val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault() | ||||
|                     val mangaList = getMangaToUpdate(intent, target) | ||||
|                             .sortedWith(rankingScheme[selectedScheme]) | ||||
|             .defer { | ||||
|                 val selectedScheme = preferences.libraryUpdatePrioritization().get() | ||||
|                 val mangaList = getMangaToUpdate(intent, target) | ||||
|                     .sortedWith(rankingScheme[selectedScheme]) | ||||
|  | ||||
|                     // Update either chapter list or manga details. | ||||
|                     when (target) { | ||||
|                         Target.CHAPTERS -> updateChapterList(mangaList) | ||||
|                         Target.DETAILS -> updateDetails(mangaList) | ||||
|                         Target.TRACKING -> updateTrackings(mangaList) | ||||
|                     } | ||||
|                 // Update either chapter list or manga details. | ||||
|                 when (target) { | ||||
|                     Target.CHAPTERS -> updateChapterList(mangaList) | ||||
|                     Target.DETAILS -> updateDetails(mangaList) | ||||
|                     Target.TRACKING -> updateTrackings(mangaList) | ||||
|                 } | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .subscribe({ | ||||
|                 }, { | ||||
|             } | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .subscribe( | ||||
|                 { | ||||
|                 }, | ||||
|                 { | ||||
|                     Timber.e(it) | ||||
|                     stopSelf(startId) | ||||
|                 }, { | ||||
|                 }, | ||||
|                 { | ||||
|                     stopSelf(startId) | ||||
|                 }) | ||||
|                 } | ||||
|             ) | ||||
|  | ||||
|         return Service.START_REDELIVER_INTENT | ||||
|         return START_REDELIVER_INTENT | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -232,18 +260,18 @@ class LibraryUpdateService( | ||||
|     fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> { | ||||
|         val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) | ||||
|  | ||||
|         var listToUpdate = if (categoryId != -1) | ||||
|         var listToUpdate = if (categoryId != -1) { | ||||
|             db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } | ||||
|         else { | ||||
|             val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt) | ||||
|             if (categoriesToUpdate.isNotEmpty()) | ||||
|         } else { | ||||
|             val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt) | ||||
|             if (categoriesToUpdate.isNotEmpty()) { | ||||
|                 db.getLibraryMangas().executeAsBlocking() | ||||
|                         .filter { it.category in categoriesToUpdate } | ||||
|                         .distinctBy { it.id } | ||||
|             else | ||||
|                     .filter { it.category in categoriesToUpdate } | ||||
|                     .distinctBy { it.id } | ||||
|             } else { | ||||
|                 db.getLibraryMangas().executeAsBlocking().distinctBy { it.id } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) { | ||||
|             listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED } | ||||
|         } | ||||
| @@ -264,66 +292,73 @@ class LibraryUpdateService( | ||||
|         // Initialize the variables holding the progress of the updates. | ||||
|         val count = AtomicInteger(0) | ||||
|         // List containing new updates | ||||
|         val newUpdates = ArrayList<Manga>() | ||||
|         // list containing failed updates | ||||
|         val newUpdates = ArrayList<Pair<LibraryManga, Array<Chapter>>>() | ||||
|         // List containing failed updates | ||||
|         val failedUpdates = ArrayList<Manga>() | ||||
|         // List containing categories that get included in downloads. | ||||
|         val categoriesToDownload = preferences.downloadNewCategories().getOrDefault().map(String::toInt) | ||||
|         val categoriesToDownload = preferences.downloadNewCategories().get().map(String::toInt) | ||||
|         // Boolean to determine if user wants to automatically download new chapters. | ||||
|         val downloadNew = preferences.downloadNew().getOrDefault() | ||||
|         val downloadNew = preferences.downloadNew().get() | ||||
|         // Boolean to determine if DownloadManager has downloads | ||||
|         var hasDownloads = false | ||||
|  | ||||
|         // Emit each manga and update it sequentially. | ||||
|         return Observable.from(mangaToUpdate) | ||||
|                 // Notify manga that will update. | ||||
|                 .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } | ||||
|                 // Update the chapters of the manga. | ||||
|                 .concatMap { manga -> | ||||
|                     if(manga.source in LIBRARY_UPDATE_EXCLUDED_SOURCES) { | ||||
|             // Notify manga that will update. | ||||
|             .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } | ||||
|             // Update the chapters of the manga. | ||||
|             .concatMap { manga -> | ||||
|             if(manga.source in LIBRARY_UPDATE_EXCLUDED_SOURCES) { | ||||
|                         // Ignore EXH manga, updating chapters for every manga will get you banned | ||||
|                         Observable.empty() | ||||
|                     } else { | ||||
|                         updateManga(manga) | ||||
|                                 // If there's any error, return empty update and continue. | ||||
|                                 .onErrorReturn { | ||||
|                                     failedUpdates.add(manga) | ||||
|                                     Pair(emptyList(), emptyList()) | ||||
|                             // If there's any error, return empty update and continue. | ||||
|                             .onErrorReturn { | ||||
|                                 failedUpdates.add(manga) | ||||
|                                 Pair(emptyList(), emptyList()) | ||||
|                             } | ||||
|                             // Filter out mangas without new chapters (or failed). | ||||
|                             .filter { pair -> pair.first.isNotEmpty() } | ||||
|                             .doOnNext { | ||||
|                                 if (downloadNew && ( | ||||
|                                     categoriesToDownload.isEmpty() || | ||||
|                                         manga.category in categoriesToDownload | ||||
|                                     ) | ||||
|                                 ) { | ||||
|                                     downloadChapters(manga, it.first) | ||||
|                                     hasDownloads = true | ||||
|                                 } | ||||
|                                 // Filter out mangas without new chapters (or failed). | ||||
|                                 .filter { pair -> pair.first.isNotEmpty() } | ||||
|                                 .doOnNext { | ||||
|                                     if (downloadNew && (categoriesToDownload.isEmpty() || | ||||
|                                                     manga.category in categoriesToDownload)) { | ||||
|  | ||||
|                                         downloadChapters(manga, it.first) | ||||
|                                         hasDownloads = true | ||||
|                                     } | ||||
|                                 } | ||||
|                                 // Convert to the manga that contains new chapters. | ||||
|                                 .map { manga } | ||||
|                             } | ||||
|                             // Convert to the manga that contains new chapters. | ||||
|                             .map { | ||||
|                                 Pair( | ||||
|                                     manga, | ||||
|                                     (it.first.sortedByDescending { ch -> ch.source_order }.toTypedArray()) | ||||
|                                 ) | ||||
|                     } | ||||
|             } | ||||
|             // Add manga with new chapters to the list. | ||||
|             .doOnNext { manga -> | ||||
|                 // Add to the list | ||||
|                 newUpdates.add(manga) | ||||
|             } | ||||
|             // Notify result of the overall update. | ||||
|             .doOnCompleted { | ||||
|                 if (newUpdates.isNotEmpty()) { | ||||
|                     showUpdateNotifications(newUpdates) | ||||
|                     if (downloadNew && hasDownloads) { | ||||
|                         DownloadService.start(this) | ||||
|                     } | ||||
|                 } | ||||
|                 // Add manga with new chapters to the list. | ||||
|                 .doOnNext { manga -> | ||||
|                     // Add to the list | ||||
|                     newUpdates.add(manga) | ||||
|                 } | ||||
|                 // Notify result of the overall update. | ||||
|                 .doOnCompleted { | ||||
|                     if (newUpdates.isNotEmpty()) { | ||||
|                         updateNotifier.showResultNotification(newUpdates) | ||||
|                         if (downloadNew && hasDownloads) { | ||||
|                             DownloadService.start(this) | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     if (failedUpdates.isNotEmpty()) { | ||||
|                         Timber.e("Failed updating: ${failedUpdates.map { it.title }}") | ||||
|                     } | ||||
|  | ||||
|                     cancelProgressNotification() | ||||
|                 if (failedUpdates.isNotEmpty()) { | ||||
|                     Timber.e("Failed updating: ${failedUpdates.map { it.title }}") | ||||
|                 } | ||||
|  | ||||
|                 cancelProgressNotification() | ||||
|             } | ||||
|             .map { manga -> manga.first } | ||||
|     } | ||||
|  | ||||
|     fun downloadChapters(manga: Manga, chapters: List<Chapter>) { | ||||
| @@ -346,7 +381,7 @@ class LibraryUpdateService( | ||||
|     fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> { | ||||
|         val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty() | ||||
|         return source.fetchChapterList(manga) | ||||
|                 .map { syncChaptersWithSource(db, it, manga, source) } | ||||
|             .map { syncChaptersWithSource(db, it, manga, source) } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -362,24 +397,24 @@ class LibraryUpdateService( | ||||
|  | ||||
|         // Emit each manga and update it sequentially. | ||||
|         return Observable.from(mangaToUpdate) | ||||
|                 // Notify manga that will update. | ||||
|                 .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } | ||||
|                 // Update the details of the manga. | ||||
|                 .concatMap { manga -> | ||||
|                     val source = sourceManager.get(manga.source) as? HttpSource | ||||
|                             ?: return@concatMap Observable.empty<LibraryManga>() | ||||
|             // Notify manga that will update. | ||||
|             .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } | ||||
|             // Update the details of the manga. | ||||
|             .concatMap { manga -> | ||||
|                 val source = sourceManager.get(manga.source) as? HttpSource | ||||
|                     ?: return@concatMap Observable.empty<LibraryManga>() | ||||
|  | ||||
|                     source.fetchMangaDetails(manga) | ||||
|                             .map { networkManga -> | ||||
|                                 manga.copyFrom(networkManga) | ||||
|                                 db.insertManga(manga).executeAsBlocking() | ||||
|                                 manga | ||||
|                             } | ||||
|                             .onErrorReturn { manga } | ||||
|                 } | ||||
|                 .doOnCompleted { | ||||
|                     cancelProgressNotification() | ||||
|                 } | ||||
|                 source.fetchMangaDetails(manga) | ||||
|                     .map { networkManga -> | ||||
|                         manga.copyFrom(networkManga) | ||||
|                         db.insertManga(manga).executeAsBlocking() | ||||
|                         manga | ||||
|                     } | ||||
|                     .onErrorReturn { manga } | ||||
|             } | ||||
|             .doOnCompleted { | ||||
|                 cancelProgressNotification() | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -394,28 +429,28 @@ class LibraryUpdateService( | ||||
|  | ||||
|         // Emit each manga and update it sequentially. | ||||
|         return Observable.from(mangaToUpdate) | ||||
|                 // Notify manga that will update. | ||||
|                 .doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) } | ||||
|                 // Update the tracking details. | ||||
|                 .concatMap { manga -> | ||||
|                     val tracks = db.getTracks(manga).executeAsBlocking() | ||||
|             // Notify manga that will update. | ||||
|             .doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) } | ||||
|             // Update the tracking details. | ||||
|             .concatMap { manga -> | ||||
|                 val tracks = db.getTracks(manga).executeAsBlocking() | ||||
|  | ||||
|                     Observable.from(tracks) | ||||
|                             .concatMap { track -> | ||||
|                                 val service = trackManager.getService(track.sync_id) | ||||
|                                 if (service != null && service in loggedServices) { | ||||
|                                     service.refresh(track) | ||||
|                                             .doOnNext { db.insertTrack(it).executeAsBlocking() } | ||||
|                                             .onErrorReturn { track } | ||||
|                                 } else { | ||||
|                                     Observable.empty() | ||||
|                                 } | ||||
|                             } | ||||
|                             .map { manga } | ||||
|                 } | ||||
|                 .doOnCompleted { | ||||
|                     cancelProgressNotification() | ||||
|                 } | ||||
|                 Observable.from(tracks) | ||||
|                     .concatMap { track -> | ||||
|                         val service = trackManager.getService(track.sync_id) | ||||
|                         if (service != null && service in loggedServices) { | ||||
|                             service.refresh(track) | ||||
|                                 .doOnNext { db.insertTrack(it).executeAsBlocking() } | ||||
|                                 .onErrorReturn { track } | ||||
|                         } else { | ||||
|                             Observable.empty() | ||||
|                         } | ||||
|                     } | ||||
|                     .map { manga } | ||||
|             } | ||||
|             .doOnCompleted { | ||||
|                 cancelProgressNotification() | ||||
|             } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -426,10 +461,116 @@ class LibraryUpdateService( | ||||
|      * @param total the total progress. | ||||
|      */ | ||||
|     private fun showProgressNotification(manga: Manga, current: Int, total: Int) { | ||||
|         notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification | ||||
|                 .setContentTitle(manga.title) | ||||
|         val title = if (preferences.hideNotificationContent()) { | ||||
|             getString(R.string.notification_check_updates) | ||||
|         } else { | ||||
|             manga.title | ||||
|         } | ||||
|  | ||||
|         notificationManager.notify( | ||||
|             Notifications.ID_LIBRARY_PROGRESS, | ||||
|             progressNotificationBuilder | ||||
|                 .setContentTitle(title) | ||||
|                 .setProgress(total, current, false) | ||||
|                 .build()) | ||||
|                 .build() | ||||
|         ) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Shows the notification containing the result of the update done by the service. | ||||
|      * | ||||
|      * @param updates a list of manga with new updates. | ||||
|      */ | ||||
|     private fun showUpdateNotifications(updates: List<Pair<Manga, Array<Chapter>>>) { | ||||
|         if (updates.isEmpty()) { | ||||
|             return | ||||
|         } | ||||
|  | ||||
|         NotificationManagerCompat.from(this).apply { | ||||
|             // Parent group notification | ||||
|             notify( | ||||
|                 Notifications.ID_NEW_CHAPTERS, | ||||
|                 notification(Notifications.CHANNEL_NEW_CHAPTERS) { | ||||
|                     setContentTitle(getString(R.string.notification_new_chapters)) | ||||
|                     if (updates.size == 1 && !preferences.hideNotificationContent()) { | ||||
|                         setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN)) | ||||
|                     } else { | ||||
|                         setContentText(resources.getQuantityString(R.plurals.notification_new_chapters_summary, updates.size, updates.size)) | ||||
|  | ||||
|                         if (!preferences.hideNotificationContent()) { | ||||
|                             setStyle( | ||||
|                                 NotificationCompat.BigTextStyle().bigText( | ||||
|                                     updates.joinToString("\n") { | ||||
|                                         it.first.title.chop(NOTIF_TITLE_MAX_LEN) | ||||
|                                     } | ||||
|                                 ) | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     setSmallIcon(R.drawable.ic_tachi) | ||||
|                     setLargeIcon(notificationBitmap) | ||||
|  | ||||
|                     setGroup(Notifications.GROUP_NEW_CHAPTERS) | ||||
|                     setGroupAlertBehavior(GROUP_ALERT_SUMMARY) | ||||
|                     setGroupSummary(true) | ||||
|                     priority = NotificationCompat.PRIORITY_HIGH | ||||
|  | ||||
|                     setContentIntent(getNotificationIntent()) | ||||
|                     setAutoCancel(true) | ||||
|                 } | ||||
|             ) | ||||
|  | ||||
|             // Per-manga notification | ||||
|             if (!preferences.hideNotificationContent()) { | ||||
|                 updates.forEach { | ||||
|                     val (manga, chapters) = it | ||||
|                     notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters)) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification { | ||||
|         return notification(Notifications.CHANNEL_NEW_CHAPTERS) { | ||||
|             setContentTitle(manga.title) | ||||
|  | ||||
|             val description = getNewChaptersDescription(chapters) | ||||
|             setContentText(description) | ||||
|             setStyle(NotificationCompat.BigTextStyle().bigText(description)) | ||||
|  | ||||
|             setSmallIcon(R.drawable.ic_tachi) | ||||
|  | ||||
|             val icon = getMangaIcon(manga) | ||||
|             if (icon != null) { | ||||
|                 setLargeIcon(icon) | ||||
|             } | ||||
|  | ||||
|             setGroup(Notifications.GROUP_NEW_CHAPTERS) | ||||
|             setGroupAlertBehavior(GROUP_ALERT_SUMMARY) | ||||
|             priority = NotificationCompat.PRIORITY_HIGH | ||||
|  | ||||
|             // Open first chapter on tap | ||||
|             setContentIntent(NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService, manga, chapters.first())) | ||||
|             setAutoCancel(true) | ||||
|  | ||||
|             // Mark chapters as read action | ||||
|             addAction( | ||||
|                 R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read), | ||||
|                 NotificationReceiver.markAsReadPendingBroadcast( | ||||
|                     this@LibraryUpdateService, | ||||
|                     manga, chapters, Notifications.ID_NEW_CHAPTERS | ||||
|                 ) | ||||
|             ) | ||||
|             // View chapters action | ||||
|             addAction( | ||||
|                 R.drawable.ic_book_24dp, getString(R.string.action_view_chapters), | ||||
|                 NotificationReceiver.openChapterPendingActivity( | ||||
|                     this@LibraryUpdateService, | ||||
|                     manga, Notifications.ID_NEW_CHAPTERS | ||||
|                 ) | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -438,4 +579,77 @@ class LibraryUpdateService( | ||||
|     private fun cancelProgressNotification() { | ||||
|         notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS) | ||||
|     } | ||||
|  | ||||
|     private fun getMangaIcon(manga: Manga): Bitmap? { | ||||
|         return try { | ||||
|             Glide.with(this) | ||||
|                 .asBitmap() | ||||
|                 .load(manga.toMangaThumbnail()) | ||||
|                 .dontTransform() | ||||
|                 .centerCrop() | ||||
|                 .circleCrop() | ||||
|                 .override(NOTIF_ICON_SIZE, NOTIF_ICON_SIZE) | ||||
|                 .submit() | ||||
|                 .get() | ||||
|         } catch (e: Exception) { | ||||
|             null | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private fun getNewChaptersDescription(chapters: Array<Chapter>): String { | ||||
|         val formatter = DecimalFormat( | ||||
|             "#.###", | ||||
|             DecimalFormatSymbols() | ||||
|                 .apply { decimalSeparator = '.' } | ||||
|         ) | ||||
|  | ||||
|         val displayableChapterNumbers = chapters | ||||
|             .filter { it.isRecognizedNumber } | ||||
|             .sortedBy { it.chapter_number } | ||||
|             .map { formatter.format(it.chapter_number) } | ||||
|             .toSet() | ||||
|  | ||||
|         return when (displayableChapterNumbers.size) { | ||||
|             // No sensible chapter numbers to show (i.e. no chapters have parsed chapter number) | ||||
|             0 -> { | ||||
|                 // "1 new chapter" or "5 new chapters" | ||||
|                 resources.getQuantityString(R.plurals.notification_chapters_generic, chapters.size, chapters.size) | ||||
|             } | ||||
|             // Only 1 chapter has a parsed chapter number | ||||
|             1 -> { | ||||
|                 val remaining = chapters.size - displayableChapterNumbers.size | ||||
|                 if (remaining == 0) { | ||||
|                     // "Chapter 2.5" | ||||
|                     resources.getString(R.string.notification_chapters_single, displayableChapterNumbers.first()) | ||||
|                 } else { | ||||
|                     // "Chapter 2.5 and 10 more" | ||||
|                     resources.getString(R.string.notification_chapters_single_and_more, displayableChapterNumbers.first(), remaining) | ||||
|                 } | ||||
|             } | ||||
|             // Everything else (i.e. multiple parsed chapter numbers) | ||||
|             else -> { | ||||
|                 val shouldTruncate = displayableChapterNumbers.size > NOTIF_MAX_CHAPTERS | ||||
|                 if (shouldTruncate) { | ||||
|                     // "Chapters 1, 2.5, 3, 4, 5 and 10 more" | ||||
|                     val remaining = displayableChapterNumbers.size - NOTIF_MAX_CHAPTERS | ||||
|                     val joinedChapterNumbers = displayableChapterNumbers.take(NOTIF_MAX_CHAPTERS).joinToString(", ") | ||||
|                     resources.getQuantityString(R.plurals.notification_chapters_multiple_and_more, remaining, joinedChapterNumbers, remaining) | ||||
|                 } else { | ||||
|                     // "Chapters 1, 2.5, 3" | ||||
|                     resources.getString(R.string.notification_chapters_multiple, displayableChapterNumbers.joinToString(", ")) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns an intent to open the main activity. | ||||
|      */ | ||||
|     private fun getNotificationIntent(): PendingIntent { | ||||
|         val intent = Intent(this, MainActivity::class.java).apply { | ||||
|             flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP | ||||
|             action = MainActivity.SHORTCUT_RECENTLY_UPDATED | ||||
|         } | ||||
|         return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -4,8 +4,9 @@ import android.app.PendingIntent | ||||
| import android.content.Context | ||||
| import android.content.Intent | ||||
| import android.net.Uri | ||||
| import eu.kanade.tachiyomi.extension.util.ExtensionInstaller | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import eu.kanade.tachiyomi.util.getUriCompat | ||||
| import eu.kanade.tachiyomi.util.storage.getUriCompat | ||||
| import java.io.File | ||||
|  | ||||
| /** | ||||
| @@ -48,7 +49,7 @@ object NotificationHandler { | ||||
|      */ | ||||
|     fun installApkPendingActivity(context: Context, uri: Uri): PendingIntent { | ||||
|         val intent = Intent(Intent.ACTION_VIEW).apply { | ||||
|             setDataAndType(uri, "application/vnd.android.package-archive") | ||||
|             setDataAndType(uri, ExtensionInstaller.APK_MIME) | ||||
|             flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION | ||||
|         } | ||||
|         return PendingIntent.getActivity(context, 0, intent, 0) | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user