merge double upstream

This commit is contained in:
Rani Sargees 2020-01-06 11:43:11 -05:00 committed by Jobobby04
commit 4d8f44ddae
900 changed files with 31103 additions and 22299 deletions

24
.gitattributes vendored Normal file
View 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

View File

@ -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: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) 2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
3. What is your type of issue? 3. What is your type of issue?
* [Catalogue request](#catalogue-requests) * [Catalogue request](#catalogue-requests)
* [Bugs](#bugs) * [Bugs](#bugs)
* [Feature requests](#feature-requests) * [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) 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 # 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) * Include screenshot (if needed)

View File

@ -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. ## Steps to reproduce
2. 1. First step
3. 2. Second step
**Other details:** ## Issue/Request
?
## Other details
Additional details and attachments.

View File

@ -3,12 +3,24 @@ name: "🐞 Bug report"
about: Report a bug about: Report a bug
title: "[Bug] Write short description here" title: "[Bug] Write short description here"
labels: "bug" 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 ### Device information
* Tachiyomi version: ? * Tachiyomi version: ?
* Android version: ? * Android version: ?
* Device: ?
## Steps to reproduce ## Steps to reproduce
1. First step 1. First step

View File

@ -3,8 +3,20 @@ name: "🌟 Feature request"
about: Suggest a feature to improve Tachiyomi about: Suggest a feature to improve Tachiyomi
title: "[Feature Request] Write short description here" title: "[Feature Request] Write short description here"
labels: "feature" 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 ### Why/User Benefit/User Problem
(explain why this feature should be added) (explain why this feature should be added)

View 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

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
View 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
View File

@ -2,14 +2,21 @@
/local.properties /local.properties
/.idea/workspace.xml /.idea/workspace.xml
.DS_Store .DS_Store
/build
.idea/ .idea/
*iml *iml
*.iml *.iml
*/build
/mainframer /mainframer
/.mainframer /.mainframer
# Built files
*/build
/build
*.apk *.apk
app/**/output.json
# Hebrew assets are copied on build
app/src/main/res/values-iw/
TODO.md TODO.md
CHANGELOG.md CHANGELOG.md
/captures /captures

View File

@ -1,59 +1,74 @@
dist: trusty dist: trusty
language: android language: android
android: android:
components: components:
- build-tools-29.0.2 - tools
- android-28 - platform-tools
- extra-android-m2repository - build-tools-29.0.3
- extra-google-m2repository - android-29
- extra-android-support - extra-android-m2repository
- extra-google-google_play_services - extra-google-m2repository
- extra-android-support
- extra-google-google_play_services
licenses: licenses:
- android-sdk-license-.+ - 'android-sdk-license-.+'
- 'android-sdk-preview-license-.+'
before_install: before_install:
- yes | sdkmanager "platforms;android-28" # workaround for accepting the license - yes | sdkmanager "platforms;android-29" # workaround for accepting the license
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then - 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; 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; tar xf secrets.tar;
mv debug.keystore "$HOME/.android"; mv debug.keystore "$HOME/.android";
fi fi
- git clone https://github.com/urho3d/android-ndk.git $HOME/android-ndk-root - mkdir "$ANDROID_HOME/licenses" || true
- export ANDROID_NDK_HOME=$HOME/android-ndk-root - echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_HOME/licenses/android-sdk-license"
- mkdir "$ANDROID_HOME/licenses" || true - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
- echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$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" script: ".travis/build.sh"
before_cache: before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/ - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache: cache:
directories: directories:
- "$HOME/.gradle/caches/" - "$HOME/.gradle/caches/"
- "$HOME/.gradle/wrapper/" - "$HOME/.gradle/wrapper/"
- "$HOME/.android/build-cache" - "$HOME/.android/build-cache"
deploy: deploy:
- provider: releases - provider: releases
api_key: api_key:
secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA= 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: tachiyomi-v*.apk
file_glob: true file_glob: true
skip_cleanup: true skip_cleanup: true
on: on:
tags: true tags: true
repo: inorichi/tachiyomi repo: inorichi/tachiyomi
- provider: script - provider: script
script: ".travis/deploy.sh" script: ".travis/deploy.sh"
skip_cleanup: true skip_cleanup: true
on: on:
branch: master branch: master
condition: "-z $TRAVIS_TAG" condition: "-z $TRAVIS_TAG"
repo: inorichi/tachiyomi repo: inorichi/tachiyomi
env: env:
global: global:
- secure: Ita1+tzo7P5IC2yqU3KgRcXt+5DTpP0103Hx/ECYi42/7rLt+TC7PMjl2yH3Z189+tGwLq0Ol1KJ2Z5Rn3q7EaQgD0+WRkH/ijtrjKoVh7dyItIBp7yowZpA0TJHQ4EZpGSxZakKbIP4di8XMxJ2+5VzEivYUt04LCUpzugemL6b6XOfUmOZykVxV2UDAlPPggklITYBXkHUa0mwJhjS1aPPeeR3PhVXomkqfuOJOKejPXXXJope9fhAnmopHA7ISfjIrTuwDVQJqNSuco+O9kQShmlu0C8pob1vFGPEDvafaDS8SZ9A6gKT1ZfgSUqVmvDbx0WLX8XugBLrQedtZv63esOa1WUyGhgFVpeJjexlszXlhyfP1gH5QbzRr+EiSaagCyjf9II2veLAtU5cFY+nj6KBdKQsazIMRHf8SAQlWASyJYMED/N9RnUFxSf1rnLGqiY2ezjycx4ieFj7vhlbTgyao1GHjjR9cwNuntdMYWhY8+Vc7Fctmzm46xOyyz9oJGdyim76Y4w4MZvQNKeZOBAjdEgX6cXBk15scoM2Vj9ENox+MKZLaKRawXg2U1ujK+bWAQkXiVvPriv05/JtYsNUft8qAsm+69vtohDsUW7Wu0bBIKDL+v0W30ty1PpyNehBB2OquUE7fp53gitOmYl7TyuxktkMY8PXKKU= - 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: 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: 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: 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: 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: 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: DKCGc4E9PKeTX68r9pbbNg5qITsN0bApQ1m0x8xdEoi8GLRKVMYNn6ahoAxvy1YsBXC9Zlt5++gLmUV1I1JyDMyJXMr/lZrp4oarW0xWpTBmn3HzOph/K2W4i/fTGgMFieumPEbQIFOnU3JSjK6UJB8qVGEXD2OqS7A//EdrGDbAYVDL3ZTKE6JUlTNHgaKaNHhn+Dq4aBLTSYPwlLyqo+WNBVUUCKCHOq62ULF8MpX5YGaPFNxKYzircV7HpF1hCbV31dmpkeYT9xztra5V0SIBM27jAcQqGmtHH2mhx1sLu+gjhFJbbtY6cggA9EedzYYLDx/NPmgfyuOJfyVbSwTF3vhDUYfskqc1THWpwOSKO0Ry+8/xYb9crxg+FSwuI5hnfkIFk9woBvRGBhjto3/1buMNY9dSFiWtEbN6Let8e747l0wIGJCpJxSeh7vn7F1mWjixhf9GX1+V9BrUvGTd3XJDNb9cVnafYa1RTj8BLteA4HBza7Z9R3dvG4YWp16L/94UuaTzgAQfERLTZGopQth/hsaVTlYesJmJLF70lGM+W83y3YuNkSaX1zQ5FAIvp7oH0O16t7ISm6GprUFwN2Uox7AAbPZdWHxJbly+D+yCFNcqS3Bz9mV3YCLo690Sy1ePNHr+nCseVfBMo7OYyavSS/EjPWfEy65Wq04=

View File

@ -1,3 +1 @@
I dont want to maintain a readme I havent started a readme
#YEET

View File

@ -2,6 +2,7 @@
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
@ -32,25 +33,23 @@ ext {
} }
android { android {
compileSdkVersion 28 compileSdkVersion 29
buildToolsVersion '29.0.2' buildToolsVersion '29.0.3'
publishNonDefault true publishNonDefault true
defaultConfig { defaultConfig {
applicationId "eu.kanade.tachiyomi.az" applicationId "eu.kanade.tachiyomi.sy"
minSdkVersion 16 minSdkVersion 21
targetSdkVersion 28 targetSdkVersion 29
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 8405 versionCode 1
versionName "v8.4.5-AZ" versionName "v0.9.2.0"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\"" buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
buildConfigField "boolean", "INCLUDE_UPDATER", "true" buildConfigField "boolean", "INCLUDE_UPDATER", "true"
vectorDrawables.useSupportLibrary = true
multiDexEnabled true multiDexEnabled true
ndk { ndk {
@ -58,6 +57,10 @@ android {
} }
} }
viewBinding {
enabled = true
}
buildTypes { buildTypes {
debug { debug {
versionNameSuffix "-${getCommitCount()}" versionNameSuffix "-${getCommitCount()}"
@ -72,8 +75,8 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
release { release {
// minifyEnabled true minifyEnabled true
// shrinkResources true shrinkResources true
zipAlignEnabled true zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
@ -127,6 +130,10 @@ android {
} }
} }
androidExtensions {
experimental = true
}
dependencies { dependencies {
// Modified dependencies // Modified dependencies
@ -134,41 +141,46 @@ dependencies {
implementation 'com.github.inorichi:junrar-android:634c1f5' implementation 'com.github.inorichi:junrar-android:634c1f5'
// Android support library // Android support library
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.cardview:cardview:1.0.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.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.annotation:annotation:1.1.0'
implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.biometric:biometric:1.0.1'
// DO NOT UPGRADE TO 17.0, IT REQUIRES ANDROIDX final lifecycle_version = '2.2.0'
standardImplementation 'com.google.firebase:firebase-core:17.2.1' 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 // ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1' implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'io.reactivex:rxjava:1.3.8' implementation 'io.reactivex:rxjava:1.3.8'
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0' 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' implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
// Network client // Network client
implementation "com.squareup.okhttp3:okhttp:4.2.1" // DO NOT UPGRADE TO 3.13.X+, it requires minSdk 21 final okhttp_version = '4.5.0'
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? implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
implementation 'com.squareup.okio:okio:2.6.0'
// REST // REST
final retrofit_version = '2.6.2' final retrofit_version = '2.8.1'
implementation "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version" implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
// JSON // 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' implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
// JavaScript engine // JavaScript engine
@ -179,12 +191,12 @@ dependencies {
implementation 'com.github.inorichi:unifile:e9ee588' implementation 'com.github.inorichi:unifile:e9ee588'
// HTML parser // HTML parser
implementation 'org.jsoup:jsoup:1.12.1' implementation 'org.jsoup:jsoup:1.13.1'
// Job scheduling // Job scheduling
implementation 'com.evernote:android-job:1.2.5' final work_version = '2.3.4'
// DO NOT UPGRADE TO 17.0, IT REQUIRES ANDROIDX implementation "androidx.work:work-runtime:$work_version"
implementation 'com.google.android.gms:play-services-gcm:17.0.0' implementation "androidx.work:work-runtime-ktx:$work_version"
// [EXH] Android 7 SSL Workaround // [EXH] Android 7 SSL Workaround
implementation 'com.google.android.gms:play-services-safetynet:17.0.0' 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' implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
// Database // 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-common:8be19de@aar'
implementation 'com.github.inorichi.storio:storio-sqlite: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 // 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:$nucleus_version"
implementation "info.android15.nucleus:nucleus-support-v7:$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" implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version"
// Transformations
implementation 'jp.wasabeef:glide-transformations:4.0.0'
// Logging // Logging
implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.jakewharton.timber:timber:4.7.1'
// Crash reports // 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 // UI
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4' implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.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 'eu.davidea:flexible-adapter-ui:1.0.0'
implementation 'com.nononsenseapps:filepicker:2.5.2' 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.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0'
implementation 'com.github.mthli:Slice:v1.3' implementation 'com.github.mthli:Slice:v1.3'
implementation 'me.gujun.android.taggroup:library:1.4@aar' implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.github.chrisbanes:PhotoView:2.3.0' // Cannot upgrade to 2.2.x+ as it uses AndroidX
implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a' 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 // Conductor
implementation 'com.bluelinelabs:conductor:2.1.5' implementation 'com.bluelinelabs:conductor:2.1.5'
implementation("com.bluelinelabs:conductor-support:2.1.5") { implementation("com.bluelinelabs:conductor-support:2.1.5") {
exclude group: "com.android.support" exclude group: "com.android.support"
} }
implementation 'com.github.inorichi:conductor-support-preference:78e2344' implementation 'com.github.inorichi:conductor-support-preference:a32c357'
// RxBindings // FlowBinding
final rxbindings_version = '1.0.1' final flowbinding_version = '0.11.1'
implementation "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version" implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
implementation "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version" implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
implementation "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version" implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
implementation "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version" implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
// Tests // Tests
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.13'
testImplementation 'org.assertj:assertj-core:1.7.1' testImplementation 'org.assertj:assertj-core:3.12.2'
testImplementation 'org.mockito:mockito-core:1.10.19' testImplementation 'org.mockito:mockito-core:1.10.19'
final robolectric_version = '3.1.4' final robolectric_version = '3.1.4'
@ -260,20 +280,22 @@ dependencies {
testImplementation "org.robolectric:shadows-multidex:$robolectric_version" testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
testImplementation "org.robolectric:shadows-play-services:$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" 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-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$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-reactive:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:$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) // Text distance (EH)
implementation 'info.debatty:java-string-similarity:1.2.1' implementation 'info.debatty:java-string-similarity:1.2.1'
// Pin lock view (EH) // Pin lock view (EH)
implementation 'com.andrognito.pinlockview:pinlockview:2.1.0' implementation 'com.github.jawnnypoo:pinlockview:2.2.0'
// Reprint (EH) // Reprint (EH)
implementation 'com.github.ajalt.reprint:core:3.2.1@aar' implementation 'com.github.ajalt.reprint:core:3.2.1@aar'
@ -283,10 +305,7 @@ dependencies {
implementation 'com.mattprecious.swirl:swirl:1.2.0' implementation 'com.mattprecious.swirl:swirl:1.2.0'
// RxJava 2 interop for Realm (EH) // RxJava 2 interop for Realm (EH)
implementation 'com.lvla.android:rxjava2-interop-kt:0.2.1' implementation 'com.github.akarnokd:rxjava2-interop:0.13.7'
// Debug network interceptor (EH)
implementation "com.squareup.okhttp3:logging-interceptor:4.2.1"
// Firebase (EH) // Firebase (EH)
implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1'
@ -323,7 +342,7 @@ dependencies {
} }
buildscript { buildscript {
ext.kotlin_version = '1.3.61' ext.kotlin_version = '1.3.72'
repositories { repositories {
mavenCentral() mavenCentral()
} }
@ -341,10 +360,15 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all {
kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"] kotlinOptions.freeCompilerArgs += ["-Xuse-experimental=kotlin.Experimental"]
} }
androidExtensions { // Duplicating Hebrew string assets due to some locale code issues on different devices
experimental = true 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")) { if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.gms.google-services'
// Firebase (EH) // Firebase (EH)

View File

@ -1,22 +1,15 @@
#-repackageclasses '' -dontobfuscate
# == Make debugging easier # Extensions may require methods unused in the core app
-renamesourcefileattribute SourceFile
-keepattributes SourceFile,LineNumberTable
# === Keep app classes
-dontwarn eu.kanade.tachiyomi.** -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 org.jsoup.** { *; }
-keep class kotlin.** { *; } -keep class kotlin.** { *; }
-keep class okhttp3.** { *; } -keep class okhttp3.** { *; }
-keep class com.google.gson.** { *; } -keep class com.google.gson.** { *; }
-keep class com.github.salomonbrys.kotson.** { *; } -keep class com.github.salomonbrys.kotson.** { *; }
-keep class com.squareup.duktape.** { *; } -keep class com.squareup.duktape.** { *; }
-keep class android.support.v7.preference.** { *; }
-keep class uy.kohesive.injekt.** { *; }
# === Keep EH classes # === Keep EH classes
-keep class exh.** { *; } -keep class exh.** { *; }
@ -25,7 +18,20 @@
# === Keep RxAndroid, https://github.com/ReactiveX/RxAndroid/issues/350 # === Keep RxAndroid, https://github.com/ReactiveX/RxAndroid/issues/350
-keep class rx.android.** { *; } -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.** -dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* { -keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
@ -130,6 +136,7 @@
# === Humanize + Guava: https://github.com/google/guava/wiki/UsingProGuardWithGuava # === Humanize + Guava: https://github.com/google/guava/wiki/UsingProGuardWithGuava
-dontwarn javax.lang.model.element.Modifier -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. # 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 # That's because the Proguard configuration required to make it work on
@ -223,10 +230,3 @@
-keep class com.google.apphosting.api.ApiProxy { -keep class com.google.apphosting.api.ApiProxy {
static *** getCurrentEnvironment (...); 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.** { *; }

View File

@ -2,7 +2,7 @@
<shortcut <shortcut
android:enabled="true" android:enabled="true"
android:icon="@drawable/sc_book_48dp" android:icon="@drawable/sc_collections_bookmark_48dp"
android:shortcutDisabledMessage="@string/app_not_available" android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_library" android:shortcutId="show_library"
android:shortcutLongLabel="@string/label_library" android:shortcutLongLabel="@string/label_library"
@ -13,7 +13,7 @@
</shortcut> </shortcut>
<shortcut <shortcut
android:enabled="true" android:enabled="true"
android:icon="@drawable/sc_update_48dp" android:icon="@drawable/sc_new_releases_48dp"
android:shortcutDisabledMessage="@string/app_not_available" android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_recently_updated" android:shortcutId="show_recently_updated"
android:shortcutLongLabel="@string/label_recent_updates" android:shortcutLongLabel="@string/label_recent_updates"
@ -24,7 +24,7 @@
</shortcut> </shortcut>
<shortcut <shortcut
android:enabled="true" android:enabled="true"
android:icon="@drawable/sc_glasses_48dp" android:icon="@drawable/sc_history_48dp"
android:shortcutDisabledMessage="@string/app_not_available" android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_recently_read" android:shortcutId="show_recently_read"
android:shortcutLongLabel="@string/label_recent_manga" android:shortcutLongLabel="@string/label_recent_manga"
@ -38,8 +38,8 @@
android:icon="@drawable/sc_explore_48dp" android:icon="@drawable/sc_explore_48dp"
android:shortcutDisabledMessage="@string/app_not_available" android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_catalogues" android:shortcutId="show_catalogues"
android:shortcutLongLabel="@string/label_catalogues" android:shortcutLongLabel="@string/browse"
android:shortcutShortLabel="@string/label_catalogues"> android:shortcutShortLabel="@string/browse">
<intent <intent
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES" android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" /> android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />

View File

@ -9,6 +9,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <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_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
@ -27,44 +28,62 @@
android:allowBackup="true" android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:hasFragileUserData="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/Theme.Tachiyomi"> android:theme="@style/Theme.Tachiyomi.Light"
android:usesCleartextTraffic="true">
<activity <activity
android:name=".ui.main.MainActivity" android:name=".ui.main.MainActivity"
android:launchMode="singleTask"> android:launchMode="singleTop"
android:theme="@style/Theme.Splash">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter> <!--suppress AndroidDomInspection -->
<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 -->
<meta-data <meta-data
android:name="android.app.shortcuts" android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" /> android:resource="@xml/shortcuts" />
</activity> </activity>
<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 <activity
android:name=".widget.CustomLayoutPickerActivity" android:name=".widget.CustomLayoutPickerActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/FilePickerTheme" /> android:theme="@style/FilePickerTheme" />
<activity <activity
android:name=".ui.setting.AnilistLoginActivity" android:name=".ui.setting.track.AnilistLoginActivity"
android:label="Anilist"> android:label="Anilist">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -78,7 +97,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".ui.setting.ShikimoriLoginActivity" android:name=".ui.setting.track.ShikimoriLoginActivity"
android:label="Shikimori"> android:label="Shikimori">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -92,7 +111,7 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".ui.setting.BangumiLoginActivity" android:name=".ui.setting.track.BangumiLoginActivity"
android:label="Bangumi"> android:label="Bangumi">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -110,6 +129,13 @@
android:name=".extension.util.ExtensionInstallActivity" android:name=".extension.util.ExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" /> 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 <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
@ -127,15 +153,19 @@
<service <service
android:name=".data.library.LibraryUpdateService" android:name=".data.library.LibraryUpdateService"
android:exported="false" /> android:exported="false" />
<service <service
android:name=".data.download.DownloadService" android:name=".data.download.DownloadService"
android:exported="false" /> android:exported="false" />
<service <service
android:name=".data.updater.UpdaterService" android:name=".data.updater.UpdaterService"
android:exported="false" /> android:exported="false" />
<service <service
android:name=".data.backup.BackupCreateService" android:name=".data.backup.BackupCreateService"
android:exported="false" /> android:exported="false" />
<service <service
android:name=".data.backup.BackupRestoreService" android:name=".data.backup.BackupRestoreService"
android:exported="false" /> android:exported="false" />
@ -273,7 +303,6 @@
<activity <activity
android:name="exh.ui.captcha.BrowserActionActivity" android:name="exh.ui.captcha.BrowserActionActivity"
android:theme="@style/Theme.EHActivity" /> android:theme="@style/Theme.EHActivity" />
<activity android:name="exh.ui.webview.WebViewActivity" />
</application> </application>
</manifest> </manifest>

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
<<<<<<< HEAD
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
@ -37,16 +38,28 @@ import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch 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 timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.registry.default.DefaultRegistrar import uy.kohesive.injekt.registry.default.DefaultRegistrar
import java.io.File import java.io.File
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import javax.net.ssl.SSLContext import javax.net.ssl.SSLContext
import kotlin.concurrent.thread import kotlin.concurrent.thread
open class App : Application() {
open class App : Application(), LifecycleObserver {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
@ -57,7 +70,6 @@ open class App : Application() {
Injekt = InjektScope(DefaultRegistrar()) Injekt = InjektScope(DefaultRegistrar())
Injekt.importModule(AppModule(this)) Injekt.importModule(AppModule(this))
setupJobManager()
setupNotificationChannels() setupNotificationChannels()
GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH) GlobalScope.launch { deleteOldMetadataRealm() } // Delete old metadata DB (EH)
Reprint.initialize(this) //Setup fingerprint (EH) Reprint.initialize(this) //Setup fingerprint (EH)
@ -66,6 +78,8 @@ open class App : Application() {
} }
LocaleHelper.updateConfiguration(this, resources.configuration) LocaleHelper.updateConfiguration(this, resources.configuration)
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
} }
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {
@ -97,18 +111,12 @@ open class App : Application() {
} }
} }
protected open fun setupJobManager() { @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
try { @Suppress("unused")
JobManager.create(this).addJobCreator { tag -> fun onAppBackgrounded() {
when (tag) { val preferences: PreferencesHelper by injectLazy()
LibraryUpdateJob.TAG -> LibraryUpdateJob() if (preferences.lockAppAfter().get() >= 0) {
UpdaterJob.TAG -> UpdaterJob() SecureActivityDelegate.locked = true
BackupCreatorJob.TAG -> BackupCreatorJob()
else -> null
}
}
} catch (e: Exception) {
Timber.w("Can't initialize job manager")
} }
} }

View File

@ -11,16 +11,17 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import exh.eh.EHentaiUpdateHelper import kotlinx.coroutines.GlobalScope
import io.noties.markwon.Markwon import kotlinx.coroutines.launch
import rx.Observable import uy.kohesive.injekt.api.InjektModule
import rx.schedulers.Schedulers import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.* import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule { class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() { override fun InjektRegistrar.registerInjectables() {
addSingleton(app) addSingleton(app)
addSingletonFactory { PreferencesHelper(app) } addSingletonFactory { PreferencesHelper(app) }
@ -49,20 +50,14 @@ class AppModule(val app: Application) : InjektModule {
// Asynchronously init expensive components for a faster cold start // 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>() } GlobalScope.launch { get<DatabaseHelper>() }
rxAsync { get<DownloadManager>() }
GlobalScope.launch { get<DownloadManager>() }
} }
private fun rxAsync(block: () -> Unit) {
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
}
} }

View File

@ -1,9 +1,11 @@
package eu.kanade.tachiyomi package eu.kanade.tachiyomi
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.data.updater.UpdaterJob
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.ui.library.LibrarySort
import java.io.File import java.io.File
object Migrations { object Migrations {
@ -18,18 +20,33 @@ object Migrations {
*/ */
fun upgrade(preferences: PreferencesHelper): Boolean { fun upgrade(preferences: PreferencesHelper): Boolean {
val context = preferences.context 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) { if (oldVersion < BuildConfig.VERSION_CODE) {
preferences.lastVersionCode().set(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) { if (oldVersion < 14) {
// Restore jobs after upgrading to evernote's job scheduler. // Restore jobs after upgrading to Evernote's job scheduler.
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) { if (BuildConfig.INCLUDE_UPDATER) {
UpdaterJob.setupTask() UpdaterJob.setupTask(context)
} }
LibraryUpdateJob.setupTask() LibraryUpdateJob.setupTask(context)
} }
if (oldVersion < 15) { if (oldVersion < 15) {
// Delete internal chapter cache dir. // Delete internal chapter cache dir.
@ -41,7 +58,7 @@ object Migrations {
if (oldDir.exists()) { if (oldDir.exists()) {
val destDir = context.getExternalFilesDir("covers") val destDir = context.getExternalFilesDir("covers")
if (destDir != null) { if (destDir != null) {
oldDir.listFiles().forEach { oldDir.listFiles()?.forEach {
it.renameTo(File(destDir, it.name)) 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 true
} }
return false return false
} }
}
}

View File

@ -1,23 +1,10 @@
package eu.kanade.tachiyomi.data.backup 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 { object BackupConst {
const val INTENT_FILTER = "SettingsBackupFragment" private const val NAME = "BackupRestoreServices"
const val ACTION_BACKUP_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED_DIALOG" const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
const val ACTION_SET_PROGRESS_DIALOG = "$ID.$INTENT_FILTER.ACTION_SET_PROGRESS_DIALOG" const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
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"
}

View File

@ -1,25 +1,22 @@
package eu.kanade.tachiyomi.data.backup package eu.kanade.tachiyomi.data.backup
import android.app.IntentService import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import com.google.gson.JsonArray import android.os.Build
import eu.kanade.tachiyomi.data.database.models.Manga import android.os.IBinder
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID 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 { companion object {
// Name of class
private const val NAME = "BackupCreateService"
// Options for backup
private const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
// Filter options // Filter options
internal const val BACKUP_CATEGORY = 0x1 internal const val BACKUP_CATEGORY = 0x1
internal const val BACKUP_CATEGORY_MASK = 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_TRACK_MASK = 0x8
internal const val BACKUP_ALL = 0xF 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 * Make a backup from library
* *
@ -38,26 +44,78 @@ class BackupCreateService : IntentService(NAME) {
* @param uri path of Uri * @param uri path of Uri
* @param flags determines what to backup * @param flags determines what to backup
*/ */
fun makeBackup(context: Context, uri: Uri, flags: Int) { fun start(context: Context, uri: Uri, flags: Int) {
val intent = Intent(context, BackupCreateService::class.java).apply { if (!isRunning(context)) {
putExtra(BackupConst.EXTRA_URI, uri) val intent = Intent(context, BackupCreateService::class.java).apply {
putExtra(EXTRA_FLAGS, flags) 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)
}
} }

View File

@ -1,42 +1,51 @@
package eu.kanade.tachiyomi.data.backup package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri import android.net.Uri
import com.evernote.android.job.Job import androidx.work.ExistingPeriodicWorkPolicy
import com.evernote.android.job.JobManager import androidx.work.PeriodicWorkRequestBuilder
import com.evernote.android.job.JobRequest 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.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import java.util.concurrent.TimeUnit
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get 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 preferences = Injekt.get<PreferencesHelper>()
val backupManager = BackupManager(context) val backupManager = BackupManager(context)
val uri = Uri.parse(preferences.backupsDirectory().getOrDefault()) val uri = Uri.parse(preferences.backupsDirectory().get())
val flags = BackupCreateService.BACKUP_ALL val flags = BackupCreateService.BACKUP_ALL
backupManager.createBackup(uri, flags, true) return try {
return Result.SUCCESS backupManager.createBackup(uri, flags, true)
Result.success()
} catch (e: Exception) {
Result.failure()
}
} }
companion object { 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 preferences = Injekt.get<PreferencesHelper>()
val interval = prefInterval ?: preferences.backupInterval().getOrDefault() val interval = prefInterval ?: preferences.backupInterval().get()
if (interval > 0) { if (interval > 0) {
JobRequest.Builder(TAG) val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000) interval.toLong(), TimeUnit.HOURS,
.setUpdateCurrent(true) 10, TimeUnit.MINUTES
.build() )
.schedule() .addTag(TAG)
} .build()
}
fun cancelTask() { WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
JobManager.instance().cancelAllForTag(TAG) } else {
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
} }
} }
} }

View File

@ -1,10 +1,16 @@
package eu.kanade.tachiyomi.data.backup package eu.kanade.tachiyomi.data.backup
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import com.github.salomonbrys.kotson.* import com.github.salomonbrys.kotson.fromJson
import com.google.gson.* 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 com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK 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.CATEGORIES
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS 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.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.HISTORY
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA 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.Backup.TRACK
import eu.kanade.tachiyomi.data.backup.models.DHistory 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.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.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.sendLocalBroadcast import kotlin.math.max
import eu.kanade.tachiyomi.util.syncChaptersWithSource
import exh.eh.EHentaiThrottleManager
import rx.Observable import rx.Observable
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) { class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
/**
* Database.
*/
internal val databaseHelper: DatabaseHelper by injectLazy() internal val databaseHelper: DatabaseHelper by injectLazy()
/**
* Source manager.
*/
internal val sourceManager: SourceManager by injectLazy() internal val sourceManager: SourceManager by injectLazy()
/**
* Tracking manager
*/
internal val trackManager: TrackManager by injectLazy() internal val trackManager: TrackManager by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
/** /**
* Version of parser * Version of parser
@ -66,11 +72,6 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*/ */
var parser: Gson = initParser() var parser: Gson = initParser()
/**
* Preferences
*/
private val preferences: PreferencesHelper by injectLazy()
/** /**
* Set version of parser * Set version of parser
* *
@ -83,7 +84,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
private fun initParser(): Gson = when (version) { private fun initParser(): Gson = when (version) {
1 -> GsonBuilder().create() 1 -> GsonBuilder().create()
2 -> GsonBuilder() 2 ->
GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build()) .registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build()) .registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build()) .registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
@ -99,7 +101,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* @param uri path of Uri * @param uri path of Uri
* @param isJob backup called from job * @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 // Create root object
val root = JsonObject() val root = JsonObject()
@ -109,24 +111,38 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
// Create category array // Create category array
val categoryEntries = JsonArray() val categoryEntries = JsonArray()
// Create extension ID/name mapping
val extensionEntries = JsonArray()
// Add value's to root // Add value's to root
root[Backup.VERSION] = Backup.CURRENT_VERSION root[Backup.VERSION] = CURRENT_VERSION
root[Backup.MANGAS] = mangaEntries root[Backup.MANGAS] = mangaEntries
root[CATEGORIES] = categoryEntries root[CATEGORIES] = categoryEntries
root[EXTENSIONS] = extensionEntries
databaseHelper.inTransaction { databaseHelper.inTransaction {
// Get manga from database // Get manga from database
val mangas = getFavoriteManga() val mangas = getFavoriteManga()
val extensions: MutableSet<String> = mutableSetOf()
// Backup library manga and its dependencies // Backup library manga and its dependencies
mangas.forEach { manga -> mangas.forEach { manga ->
mangaEntries.add(backupMangaObject(manga, flags)) 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 // Backup categories
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) { if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
backupCategories(categoryEntries) backupCategories(categoryEntries)
} }
// Backup extension ID/name mapping
backupExtensionInfo(extensionEntries, extensions)
} }
try { try {
@ -140,42 +156,38 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
val numberOfBackups = numberOfBackups() val numberOfBackups = numberOfBackups()
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""") val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
dir.listFiles { _, filename -> backupRegex.matches(filename) } dir.listFiles { _, filename -> backupRegex.matches(filename) }
.orEmpty() .orEmpty()
.sortedByDescending { it.name } .sortedByDescending { it.name }
.drop(numberOfBackups - 1) .drop(numberOfBackups - 1)
.forEach { it.delete() } .forEach { it.delete() }
// Create new file to place backup // Create new file to place backup
val newFile = dir.createFile(Backup.getDefaultFilename()) val newFile = dir.createFile(Backup.getDefaultFilename())
?: throw Exception("Couldn't create backup file") ?: throw Exception("Couldn't create backup file")
newFile.openOutputStream().bufferedWriter().use { newFile.openOutputStream().bufferedWriter().use {
parser.toJson(root, it) parser.toJson(root, it)
} }
return newFile.uri.toString()
} else { } else {
val file = UniFile.fromUri(context, uri) val file = UniFile.fromUri(context, uri)
?: throw Exception("Couldn't create backup file") ?: throw Exception("Couldn't create backup file")
file.openOutputStream().bufferedWriter().use { file.openOutputStream().bufferedWriter().use {
parser.toJson(root, it) parser.toJson(root, it)
} }
// Show completed dialog return file.uri.toString()
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)
} }
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e) Timber.e(e)
if (!isJob) { throw e
// 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) private fun backupExtensionInfo(root: JsonArray, extensions: Set<String>) {
} extensions.sorted().forEach {
context.sendLocalBroadcast(intent) root.add(it)
}
} }
} }
@ -206,7 +218,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) { if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
// Backup all the chapters // Backup all the chapters
val chapters = databaseHelper.getChapters(manga).executeAsBlocking() val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
if (!chapters.isEmpty()) { if (chapters.isNotEmpty()) {
val chaptersJson = parser.toJsonTree(chapters) val chaptersJson = parser.toJsonTree(chapters)
if (chaptersJson.asJsonArray.size() > 0) { if (chaptersJson.asJsonArray.size() > 0) {
entry[CHAPTERS] = chaptersJson entry[CHAPTERS] = chaptersJson
@ -218,7 +230,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
// Backup categories for this manga // Backup categories for this manga
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking() val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
if (!categoriesForManga.isEmpty()) { if (categoriesForManga.isNotEmpty()) {
val categoriesNames = categoriesForManga.map { it.name } val categoriesNames = categoriesForManga.map { it.name }
entry[CATEGORIES] = parser.toJsonTree(categoriesNames) 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 // Check if user wants track information in backup
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) { if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
val tracks = databaseHelper.getTracks(manga).executeAsBlocking() val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
if (!tracks.isEmpty()) { if (tracks.isNotEmpty()) {
entry[TRACK] = parser.toJsonTree(tracks) 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 // Check if user wants history information in backup
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) { if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking() val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
if (!historyForManga.isEmpty()) { if (historyForManga.isNotEmpty()) {
val historyData = historyForManga.mapNotNull { history -> val historyData = historyForManga.mapNotNull { history ->
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
url?.let { DHistory(url, history.last_read) } 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> { fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
return source.fetchMangaDetails(manga) return source.fetchMangaDetails(manga)
.map { networkManga -> .map { networkManga ->
manga.copyFrom(networkManga) manga.copyFrom(networkManga)
manga.favorite = true manga.favorite = true
manga.initialized = true manga.initialized = true
manga.id = insertManga(manga) manga.id = insertManga(manga)
manga manga
} }
} }
/** /**
@ -282,18 +294,18 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* @param manga manga that needs updating * @param manga manga that needs updating
* @return [Observable] that contains manga * @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) { return (if(source is EHentai) {
source.fetchChapterList(manga, throttleManager::throttle) source.fetchChapterList(manga, throttleManager::throttle)
} else { } else {
source.fetchChapterList(manga) source.fetchChapterList(manga)
}).map { syncChaptersWithSource(databaseHelper, it, manga, source) } .map { syncChaptersWithSource(databaseHelper, it, manga, source) }
.doOnNext { .doOnNext { pair ->
if (it.first.isNotEmpty()) { if (pair.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id } chapters.forEach { it.manga_id = manga.id }
insertChapters(chapters) insertChapters(chapters)
}
} }
}
} }
/** /**
@ -349,7 +361,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
} }
// Update database // Update database
if (!mangaCategoriesToUpdate.isEmpty()) { if (mangaCategoriesToUpdate.isNotEmpty()) {
val mangaAsList = ArrayList<Manga>() val mangaAsList = ArrayList<Manga>()
mangaAsList.add(manga) mangaAsList.add(manga)
databaseHelper.deleteOldMangasCategories(mangaAsList).executeAsBlocking() 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 // Check if history already in database and update
if (dbHistory != null) { if (dbHistory != null) {
dbHistory.apply { dbHistory.apply {
last_read = Math.max(lastRead, dbHistory.last_read) last_read = max(lastRead, dbHistory.last_read)
} }
historyToBeUpdated.add(dbHistory) historyToBeUpdated.add(dbHistory)
} else { } else {
@ -413,7 +425,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
if (track.library_id != dbTrack.library_id) { if (track.library_id != dbTrack.library_id) {
dbTrack.library_id = track.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 isInDatabase = true
trackToUpdate.add(dbTrack) trackToUpdate.add(dbTrack)
break break
@ -427,7 +439,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
} }
} }
// Update database // Update database
if (!trackToUpdate.isEmpty()) { if (trackToUpdate.isNotEmpty()) {
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking() databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
} }
} }
@ -443,8 +455,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking() val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
// Return if fetch is needed // Return if fetch is needed
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
return false return false
}
for (chapter in chapters) { for (chapter in chapters) {
val pos = dbChapters.indexOf(chapter) val pos = dbChapters.indexOf(chapter)
@ -469,7 +482,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* @return [Manga], null if not found * @return [Manga], null if not found
*/ */
internal fun getMangaFromDatabase(manga: Manga): Manga? = 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 * Returns list containing manga from library
@ -477,7 +490,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* @return [Manga] from library * @return [Manga] from library
*/ */
internal fun getFavoriteManga(): List<Manga> = internal fun getFavoriteManga(): List<Manga> =
databaseHelper.getFavoriteMangas().executeAsBlocking() databaseHelper.getFavoriteMangas().executeAsBlocking()
/** /**
* Inserts manga and returns id * 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 * @return id of [Manga], null if not found
*/ */
internal fun insertManga(manga: Manga): Long? = internal fun insertManga(manga: Manga): Long? =
databaseHelper.insertManga(manga).executeAsBlocking().insertedId() databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
/** /**
* Inserts list of chapters * Inserts list of chapters
@ -499,5 +512,5 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* *
* @return number of backups selected by user * @return number of backups selected by user
*/ */
fun numberOfBackups(): Int = preferences.numberOfBackups().getOrDefault() fun numberOfBackups(): Int = preferences.numberOfBackups().get()
} }

View File

@ -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)
}
}
}

View File

@ -10,6 +10,8 @@ import android.os.PowerManager
import com.elvishew.xlog.XLog import com.elvishew.xlog.XLog
import com.github.salomonbrys.kotson.fromJson import com.github.salomonbrys.kotson.fromJson
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.R 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.Backup.VERSION
import eu.kanade.tachiyomi.data.backup.models.DHistory import eu.kanade.tachiyomi.data.backup.models.DHistory
import eu.kanade.tachiyomi.data.database.DatabaseHelper 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.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.chop import eu.kanade.tachiyomi.util.system.isServiceRunning
import eu.kanade.tachiyomi.util.isServiceRunning
import eu.kanade.tachiyomi.util.sendLocalBroadcast
import exh.BackupEntry import exh.BackupEntry
import exh.EH_SOURCE_ID import exh.EH_SOURCE_ID
import exh.EXHMigrations import exh.EXHMigrations
@ -42,11 +48,16 @@ import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import java.util.concurrent.ExecutorService import kotlinx.coroutines.CoroutineExceptionHandler
import java.util.concurrent.Executors 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() { class BackupRestoreService : Service() {
@ -58,8 +69,8 @@ class BackupRestoreService : Service() {
* @param context the application context. * @param context the application context.
* @return true if the service is running, false otherwise. * @return true if the service is running, false otherwise.
*/ */
private fun isRunning(context: Context): Boolean = fun isRunning(context: Context): Boolean =
context.isServiceRunning(BackupRestoreService::class.java) context.isServiceRunning(BackupRestoreService::class.java)
/** /**
* Starts a service to restore a backup from Json * Starts a service to restore a backup from Json
@ -72,7 +83,11 @@ class BackupRestoreService : Service() {
val intent = Intent(context, BackupRestoreService::class.java).apply { val intent = Intent(context, BackupRestoreService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri) 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) { fun stop(context: Context) {
context.stopService(Intent(context, BackupRestoreService::class.java)) 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 private lateinit var wakeLock: PowerManager.WakeLock
/** private var job: Job? = null
* Subscription where the update is done.
*/
private var subscription: Subscription? = null
/** /**
* The progress of a backup restore * The progress of a backup restore
@ -111,20 +125,12 @@ class BackupRestoreService : Service() {
*/ */
private val errors = mutableListOf<Pair<Date, String>>() private val errors = mutableListOf<Pair<Date, String>>()
/**
* Backup manager
*/
private lateinit var backupManager: BackupManager private lateinit var backupManager: BackupManager
private lateinit var notifier: BackupNotifier
/**
* Database
*/
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
/** private val trackManager: TrackManager by injectLazy()
* Tracking manager
*/
internal val trackManager: TrackManager by injectLazy()
private lateinit var executor: ExecutorService private lateinit var executor: ExecutorService
@ -136,23 +142,31 @@ class BackupRestoreService : Service() {
*/ */
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
notifier = BackupNotifier(this)
startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build())
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock") PowerManager.PARTIAL_WAKE_LOCK, "${javaClass.name}:WakeLock"
)
wakeLock.acquire() wakeLock.acquire()
executor = Executors.newSingleThreadExecutor()
} }
/** override fun stopService(name: Intent?): Boolean {
* Method called when the service is destroyed. It destroys the running subscription and destroyJob()
* releases the wake lock. return super.stopService(name)
*/ }
override fun onDestroy() { override fun onDestroy() {
subscription?.unsubscribe() destroyJob()
executor.shutdown() // must be called after unsubscribe super.onDestroy()
}
private fun destroyJob() {
job?.cancel()
if (wakeLock.isHeld) { if (wakeLock.isHeld) {
wakeLock.release() wakeLock.release()
} }
super.onDestroy()
} }
/** /**
@ -169,157 +183,109 @@ class BackupRestoreService : Service() {
* @return the start value of the command. * @return the start value of the command.
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 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. stopSelf(startId)
subscription?.unsubscribe() }
job = GlobalScope.launch(handler) {
restoreBackup(uri)
}
job?.invokeOnCompletion {
stopSelf(startId)
}
subscription = Observable.using( return START_NOT_STICKY
{
// 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
} }
/** /**
* Returns an [Observable] containing restore process. * Restores data from backup file.
* *
* @param uri restore file * @param uri backup file to restore
* @return [Observable<Manga>]
*/ */
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> { private fun restoreBackup(uri: Uri) {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
return Observable.just(Unit) val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
.map { val json = JsonParser.parseReader(reader).asJsonObject
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
val json = JsonParser().parse(reader).asJsonObject
// Get parser version // Get parser version
val version = json.get(VERSION)?.asInt ?: 1 val version = json.get(VERSION)?.asInt ?: 1
// Initialize manager // Initialize manager
backupManager = BackupManager(this, version) backupManager = BackupManager(this, version)
val mangasJson = json.get(MANGAS).asJsonArray val mangasJson = json.get(MANGAS).asJsonArray
restoreAmount = mangasJson.size() + 1 // +1 for categories restoreAmount = mangasJson.size() + 1 // +1 for categories
restoreProgress = 0 restoreProgress = 0
errors.clear() errors.clear()
// Restore categories // Restore categories
json.get(CATEGORIES)?.let { restoreCategories(json.get(CATEGORIES))
backupManager.restoreCategories(it.asJsonArray)
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
}
mangasJson // Restore individual manga
} mangasJson.forEach {
.flatMap { Observable.from(it) } restoreManga(it.asJsonObject)
.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())
// EXH --> val endTime = System.currentTimeMillis()
val migrated = EXHMigrations.migrateBackupEntry( val time = endTime - startTime
BackupEntry(
manga,
chapters,
categories,
history,
tracks
)
)
val observable = migrated.flatMap { (manga, chapters, categories, history, tracks) -> val logFile = writeErrorLog()
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)
} notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
.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() }
} }
/** private fun restoreCategories(categoriesJson: JsonElement) {
* Write errors to error log db.inTransaction {
*/ backupManager.restoreCategories(categoriesJson.asJsonArray)
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())
destFile.bufferedWriter().use { out -> restoreProgress += 1
errors.forEach { (date, message) -> showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
out.write("[${sdf.format(date)}] $message\n") }
} }
}
return destFile private fun restoreManga(mangaJson: JsonObject) {
} db.inTransaction {
} catch (e: Exception) { val manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA))
// Empty 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 categories categories data from json
* @param history history data from json * @param history history data from json
* @param tracks tracking data from json * @param tracks tracking data from json
* @return [Observable] containing manga restore information
*/ */
private fun getMangaRestoreObservable(manga: Manga, chapters: List<Chapter>, private fun restoreMangaData(
categories: List<String>, history: List<DHistory>, manga: Manga,
tracks: List<Track>): Observable<Manga>? { chapters: List<Chapter>,
categories: List<String>,
history: List<DHistory>,
tracks: List<Track>
) {
// Get source // Get source
val source = backupManager.sourceManager.getOrStub(manga.source) val source = backupManager.sourceManager.getOrStub(manga.source)
val dbManga = backupManager.getMangaFromDatabase(manga) val dbManga = backupManager.getMangaFromDatabase(manga)
return if (dbManga == null) { if (dbManga == null) {
// Manga not in database // Manga not in database
mangaFetchObservable(source, manga, chapters, categories, history, tracks) restoreMangaFetch(source, manga, chapters, categories, history, tracks)
} else { // Manga in database } else { // Manga in database
// Copy information from manga already in database // Copy information from manga already in database
backupManager.restoreMangaNoFetch(manga, dbManga) backupManager.restoreMangaNoFetch(manga, dbManga)
// Fetch rest of manga information // 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 chapters chapters of manga that needs updating
* @param categories categories that need updating * @param categories categories that need updating
*/ */
private fun mangaFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>, private fun restoreMangaFetch(
categories: List<String>, history: List<DHistory>, source: Source,
tracks: List<Track>): Observable<Manga> { manga: Manga,
if(source.id == EH_SOURCE_ID || source.id == EXH_SOURCE_ID) chapters: List<Chapter>,
throttleManager.throttle() categories: List<String>,
history: List<DHistory>,
return backupManager.restoreMangaFetchObservable(source, manga) tracks: List<Track>
.onErrorReturn { ) {
// [EXH] backupManager.restoreMangaFetchObservable(source, manga)
XLog.w("> Failed to restore manga!", it) .onErrorReturn {
XLog.w("> (source.id: %s, source.name: %s, manga.id: %s, manga.url: %s)", errors.add(Date() to "${manga.title} - ${it.message}")
source.id, manga
source.name, }
manga.id, .filter { it.id != null }
manga.url) .flatMap {
chapterFetchObservable(source, it, chapters)
errors.add(Date() to "${manga.title} - ${it.message}") // Convert to the manga that contains new chapters.
manga .map { manga }
} }
.filter { it.id != null } .doOnNext {
.flatMap { restoreExtraForManga(it, categories, history, tracks)
chapterFetchObservable(source, it, chapters) }
// Convert to the manga that contains new chapters. .flatMap {
.map { manga } trackingFetchObservable(it, tracks)
} }
.doOnNext { .subscribe()
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 mangaNoFetchObservable(source: Source, backupManga: Manga, chapters: List<Chapter>, private fun restoreMangaNoFetch(
categories: List<String>, history: List<DHistory>, source: Source,
tracks: List<Track>): Observable<Manga> { backupManga: Manga,
chapters: List<Chapter>,
return Observable.just(backupManga) categories: List<String>,
.flatMap { manga -> history: List<DHistory>,
if (!backupManager.restoreChaptersForManga(manga, chapters)) { tracks: List<Track>
chapterFetchObservable(source, manga, chapters) ) {
.map { manga } Observable.just(backupManga)
} else { .flatMap { manga ->
Observable.just(manga) if (!backupManager.restoreChaptersForManga(manga, chapters)) {
} chapterFetchObservable(source, manga, chapters)
} .map { manga }
.doOnNext { } else {
restoreExtraForManga(it, categories, history, tracks) Observable.just(manga)
}
.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)
} }
}
.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>) { 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 * @return [Observable] that contains manga
*/ */
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> { private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
return backupManager.restoreChapterFetchObservable(source, manga, chapters, throttleManager) return backupManager.restoreChapterFetchObservable(source, manga, chapters)
// If there's any error, return empty update and continue. // If there's any error, return empty update and continue.
.onErrorReturn { .onErrorReturn {
// [EXH] errors.add(Date() to "${manga.title} - ${it.message}")
XLog.w("> Failed to restore chapter!", it) Pair(emptyList(), emptyList())
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())
}
} }
/** /**
@ -467,20 +415,20 @@ class BackupRestoreService : Service() {
*/ */
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> { private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
return Observable.from(tracks) return Observable.from(tracks)
.concatMap { track -> .concatMap { track ->
val service = trackManager.getService(track.sync_id) val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) { if (service != null && service.isLogged) {
service.refresh(track) service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() } .doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn { .onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}") errors.add(Date() to "${manga.title} - ${it.message}")
track track
} }
} else { } else {
errors.add(Date() to "${manga.title} - ${service?.name} not logged in") errors.add(Date() to "${manga.title} - ${service?.name} not logged in")
Observable.empty() Observable.empty()
}
} }
}
} }
/** /**
@ -490,16 +438,33 @@ class BackupRestoreService : Service() {
* @param amount total restoreAmount of manga * @param amount total restoreAmount of manga
* @param title title of restored manga * @param title title of restored manga
*/ */
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int, private fun showRestoreProgress(
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) { progress: Int,
val intent = Intent(BackupConst.INTENT_FILTER).apply { amount: Int,
putExtra(BackupConst.EXTRA_PROGRESS, progress) title: String
putExtra(BackupConst.EXTRA_AMOUNT, amount) ) {
putExtra(BackupConst.EXTRA_CONTENT, content) notifier.showRestoreProgress(title, progress, amount)
putExtra(BackupConst.EXTRA_ERRORS, errors)
putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG)
}
sendLocalBroadcast(intent)
} }
/**
* 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("")
}
} }

View File

@ -1,23 +1,25 @@
package eu.kanade.tachiyomi.data.backup.models package eu.kanade.tachiyomi.data.backup.models
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.Date
import java.util.Locale
/**
* Json values /**
*/ * Json values
object Backup { */
const val CURRENT_VERSION = 2 object Backup {
const val MANGA = "manga" const val CURRENT_VERSION = 2
const val MANGAS = "mangas" const val MANGA = "manga"
const val TRACK = "track" const val MANGAS = "mangas"
const val CHAPTERS = "chapters" const val TRACK = "track"
const val CATEGORIES = "categories" const val CHAPTERS = "chapters"
const val HISTORY = "history" const val CATEGORIES = "categories"
const val VERSION = "version" const val EXTENSIONS = "extensions"
const val HISTORY = "history"
fun getDefaultFilename(): String { const val VERSION = "version"
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
return "tachiyomi_$date.json" fun getDefaultFilename(): String {
} val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
} return "tachiyomi_$date.json"
}
}

View File

@ -1,3 +1,3 @@
package eu.kanade.tachiyomi.data.backup.models 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)

View File

@ -28,4 +28,4 @@ object CategoryTypeAdapter {
} }
} }
} }
} }

View File

@ -43,9 +43,7 @@ object ChapterTypeAdapter {
beginObject() beginObject()
while (hasNext()) { while (hasNext()) {
if (peek() == JsonToken.NAME) { if (peek() == JsonToken.NAME) {
val name = nextName() when (nextName()) {
when (name) {
URL -> chapter.url = nextString() URL -> chapter.url = nextString()
READ -> chapter.read = nextInt() == 1 READ -> chapter.read = nextInt() == 1
BOOKMARK -> chapter.bookmark = nextInt() == 1 BOOKMARK -> chapter.bookmark = nextInt() == 1
@ -58,4 +56,4 @@ object ChapterTypeAdapter {
} }
} }
} }
} }

View File

@ -29,4 +29,4 @@ object HistoryTypeAdapter {
} }
} }
} }
} }

View File

@ -34,4 +34,4 @@ object MangaTypeAdapter {
} }
} }
} }
} }

View File

@ -41,9 +41,7 @@ object TrackTypeAdapter {
beginObject() beginObject()
while (hasNext()) { while (hasNext()) {
if (peek() == JsonToken.NAME) { if (peek() == JsonToken.NAME) {
val name = nextName() when (nextName()) {
when (name) {
TITLE -> track.title = nextString() TITLE -> track.title = nextString()
SYNC -> track.sync_id = nextInt() SYNC -> track.sync_id = nextInt()
MEDIA -> track.media_id = nextInt() MEDIA -> track.media_id = nextInt()
@ -58,4 +56,4 @@ object TrackTypeAdapter {
} }
} }
} }
} }

View File

@ -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.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
import java.io.File
import java.io.IOException
import okhttp3.Response import okhttp3.Response
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
/** /**
* Class used to create chapter cache * Class used to create chapter cache
@ -29,6 +29,7 @@ import java.io.IOException
* @constructor creates an instance of the chapter cache. * @constructor creates an instance of the chapter cache.
*/ */
class ChapterCache(private val context: Context) { class ChapterCache(private val context: Context) {
companion object { companion object {
/** Name of cache directory. */ /** Name of cache directory. */
const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache" const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
@ -96,16 +97,17 @@ class ChapterCache(private val context: Context) {
*/ */
fun removeFileFromCache(file: String): Boolean { fun removeFileFromCache(file: String): Boolean {
// Make sure we don't delete the journal file (keeps track of cache). // 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 return false
}
try { return try {
// Remove the extension from the file to get the key of the cache // Remove the extension from the file to get the key of the cache
val key = file.substringBeforeLast(".") val key = file.substringBeforeLast(".")
// Remove file from cache. // Remove file from cache.
return diskCache.remove(key) diskCache.remove(key)
} catch (e: Exception) { } catch (e: Exception) {
return false false
} }
} }
@ -154,7 +156,6 @@ class ChapterCache(private val context: Context) {
diskCache.flush() diskCache.flush()
editor.commit() editor.commit()
editor.abortUnlessCommitted() editor.abortUnlessCommitted()
} catch (e: Exception) { } catch (e: Exception) {
// Ignore. // Ignore.
} finally { } finally {
@ -169,10 +170,10 @@ class ChapterCache(private val context: Context) {
* @return true if in cache otherwise false. * @return true if in cache otherwise false.
*/ */
fun isImageInCache(imageUrl: String): Boolean { fun isImageInCache(imageUrl: String): Boolean {
try { return try {
return diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)) != null diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)) != null
} catch (e: IOException) { } catch (e: IOException) {
return false false
} }
} }
@ -190,7 +191,7 @@ class ChapterCache(private val context: Context) {
/** /**
* Add image to cache. * Add image to cache.
* *
* @param imageUrl url of image. * @param imageUrl url of image.
* @param response http response from page. * @param response http response from page.
* @throws IOException image error. * @throws IOException image error.
@ -220,4 +221,3 @@ class ChapterCache(private val context: Context) {
return "${chapter.manga_id}${chapter.url}" return "${chapter.manga_id}${chapter.url}"
} }
} }

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.cache package eu.kanade.tachiyomi.data.cache
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.util.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -20,8 +20,8 @@ class CoverCache(private val context: Context) {
/** /**
* Cache directory used for cache management. * Cache directory used for cache management.
*/ */
private val cacheDir = context.getExternalFilesDir("covers") ?: private val cacheDir = context.getExternalFilesDir("covers")
File(context.filesDir, "covers").also { it.mkdirs() } ?: File(context.filesDir, "covers").also { it.mkdirs() }
/** /**
* Returns the cover from cache. * Returns the cover from cache.
@ -37,7 +37,7 @@ class CoverCache(private val context: Context) {
* Copy the given stream to this cache. * Copy the given stream to this cache.
* *
* @param thumbnailUrl url of the thumbnail. * @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 if there's any error.
*/ */
@Throws(IOException::class) @Throws(IOException::class)
@ -56,12 +56,12 @@ class CoverCache(private val context: Context) {
*/ */
fun deleteFromCache(thumbnailUrl: String?): Boolean { fun deleteFromCache(thumbnailUrl: String?): Boolean {
// Check if url is empty. // Check if url is empty.
if (thumbnailUrl.isNullOrEmpty()) if (thumbnailUrl.isNullOrEmpty()) {
return false return false
}
// Remove file. // Remove file.
val file = getCoverFile(thumbnailUrl) val file = getCoverFile(thumbnailUrl)
return file.exists() && file.delete() return file.exists() && file.delete()
} }
} }

View File

@ -3,46 +3,51 @@ package eu.kanade.tachiyomi.data.database
import android.content.Context import android.content.Context
import androidx.sqlite.db.SupportSQLiteOpenHelper import androidx.sqlite.db.SupportSQLiteOpenHelper
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
import eu.kanade.tachiyomi.data.database.mappers.* import eu.kanade.tachiyomi.data.database.mappers.CategoryTypeMapping
import eu.kanade.tachiyomi.data.database.models.* import eu.kanade.tachiyomi.data.database.mappers.ChapterTypeMapping
import eu.kanade.tachiyomi.data.database.queries.* import eu.kanade.tachiyomi.data.database.mappers.HistoryTypeMapping
import exh.metadata.sql.mappers.SearchMetadataTypeMapping import eu.kanade.tachiyomi.data.database.mappers.MangaCategoryTypeMapping
import exh.metadata.sql.mappers.SearchTagTypeMapping import eu.kanade.tachiyomi.data.database.mappers.MangaTypeMapping
import exh.metadata.sql.mappers.SearchTitleTypeMapping import eu.kanade.tachiyomi.data.database.mappers.TrackTypeMapping
import exh.metadata.sql.models.SearchMetadata import eu.kanade.tachiyomi.data.database.models.Category
import exh.metadata.sql.models.SearchTag import eu.kanade.tachiyomi.data.database.models.Chapter
import exh.metadata.sql.models.SearchTitle import eu.kanade.tachiyomi.data.database.models.History
import exh.metadata.sql.queries.SearchMetadataQueries import eu.kanade.tachiyomi.data.database.models.Manga
import exh.metadata.sql.queries.SearchTagQueries import eu.kanade.tachiyomi.data.database.models.MangaCategory
import exh.metadata.sql.queries.SearchTitleQueries 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 import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
/** /**
* This class provides operations to manage the database through its interfaces. * This class provides operations to manage the database through its interfaces.
*/ */
open class DatabaseHelper(context: Context) open class DatabaseHelper(context: Context) :
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries, MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries, /* EXH --> */ SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */ {
/* EXH --> */ SearchMetadataQueries, SearchTagQueries, SearchTitleQueries /* EXH <-- */
{
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(DbOpenCallback.DATABASE_NAME) .name(DbOpenCallback.DATABASE_NAME)
.callback(DbOpenCallback()) .callback(DbOpenCallback())
.build() .build()
override val db = DefaultStorIOSQLite.builder() override val db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration)) .sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration))
.addTypeMapping(Manga::class.java, MangaTypeMapping()) .addTypeMapping(Manga::class.java, MangaTypeMapping())
.addTypeMapping(Chapter::class.java, ChapterTypeMapping()) .addTypeMapping(Chapter::class.java, ChapterTypeMapping())
.addTypeMapping(Track::class.java, TrackTypeMapping()) .addTypeMapping(Track::class.java, TrackTypeMapping())
.addTypeMapping(Category::class.java, CategoryTypeMapping()) .addTypeMapping(Category::class.java, CategoryTypeMapping())
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping()) .addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
.addTypeMapping(History::class.java, HistoryTypeMapping()) .addTypeMapping(History::class.java, HistoryTypeMapping())
// EXH --> // EXH -->
.addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping()) .addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping())
.addTypeMapping(SearchTag::class.java, SearchTagTypeMapping()) .addTypeMapping(SearchTag::class.java, SearchTagTypeMapping())
.addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping()) .addTypeMapping(SearchTitle::class.java, SearchTitleTypeMapping())
// EXH <-- // EXH <--
.build() .build()
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block) inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)

View File

@ -22,4 +22,3 @@ inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
lowLevel().endTransaction() lowLevel().endTransaction()
} }
} }

View File

@ -2,7 +2,12 @@ package eu.kanade.tachiyomi.data.database
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper 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.SearchMetadataTable
import exh.metadata.sql.tables.SearchTagTable import exh.metadata.sql.tables.SearchTagTable
import exh.metadata.sql.tables.SearchTitleTable import exh.metadata.sql.tables.SearchTitleTable
@ -18,7 +23,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/** /**
* Version of the database. * Version of the database.
*/ */
const val DATABASE_VERSION = 9 // [EXH] const val DATABASE_VERSION = 0 // [SY]
} }
override fun onCreate(db: SupportSQLiteDatabase) = with(db) { 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) { override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion < 2) { if (oldVersion < 0) {
db.execSQL(ChapterTable.sourceOrderUpdateQuery) db.execSQL(ChapterTable.sourceOrderUpdateQuery)
// Fix kissmanga covers after supporting cloudflare // Fix kissmanga covers after supporting cloudflare
db.execSQL("""UPDATE mangas SET thumbnail_url = db.execSQL(
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""") """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) { override fun onConfigure(db: SupportSQLiteDatabase) {
db.setForeignKeyConstraintsEnabled(true) db.setForeignKeyConstraintsEnabled(true)
} }
} }

View File

@ -5,5 +5,4 @@ import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
interface DbProvider { interface DbProvider {
val db: DefaultStorIOSQLite val db: DefaultStorIOSQLite
}
}

View File

@ -18,22 +18,22 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
class CategoryTypeMapping : SQLiteTypeMapping<Category>( class CategoryTypeMapping : SQLiteTypeMapping<Category>(
CategoryPutResolver(), CategoryPutResolver(),
CategoryGetResolver(), CategoryGetResolver(),
CategoryDeleteResolver() CategoryDeleteResolver()
) )
class CategoryPutResolver : DefaultPutResolver<Category>() { class CategoryPutResolver : DefaultPutResolver<Category>() {
override fun mapToInsertQuery(obj: Category) = InsertQuery.builder() override fun mapToInsertQuery(obj: Category) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: Category) = ContentValues(4).apply { override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
@ -56,8 +56,8 @@ class CategoryGetResolver : DefaultGetResolver<Category>() {
class CategoryDeleteResolver : DefaultDeleteResolver<Category>() { class CategoryDeleteResolver : DefaultDeleteResolver<Category>() {
override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

@ -26,22 +26,22 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>( class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
ChapterPutResolver(), ChapterPutResolver(),
ChapterGetResolver(), ChapterGetResolver(),
ChapterDeleteResolver() ChapterDeleteResolver()
) )
class ChapterPutResolver : DefaultPutResolver<Chapter>() { class ChapterPutResolver : DefaultPutResolver<Chapter>() {
override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder() override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply { override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
@ -80,9 +80,8 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() {
class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() { class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

@ -18,22 +18,22 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
class HistoryTypeMapping : SQLiteTypeMapping<History>( class HistoryTypeMapping : SQLiteTypeMapping<History>(
HistoryPutResolver(), HistoryPutResolver(),
HistoryGetResolver(), HistoryGetResolver(),
HistoryDeleteResolver() HistoryDeleteResolver()
) )
open class HistoryPutResolver : DefaultPutResolver<History>() { open class HistoryPutResolver : DefaultPutResolver<History>() {
override fun mapToInsertQuery(obj: History) = InsertQuery.builder() override fun mapToInsertQuery(obj: History) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: History) = ContentValues(4).apply { override fun mapToContentValues(obj: History) = ContentValues(4).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
@ -56,8 +56,8 @@ class HistoryGetResolver : DefaultGetResolver<History>() {
class HistoryDeleteResolver : DefaultDeleteResolver<History>() { class HistoryDeleteResolver : DefaultDeleteResolver<History>() {
override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

@ -16,22 +16,22 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>( class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
MangaCategoryPutResolver(), MangaCategoryPutResolver(),
MangaCategoryGetResolver(), MangaCategoryGetResolver(),
MangaCategoryDeleteResolver() MangaCategoryDeleteResolver()
) )
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() { class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder() override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply { override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
@ -52,8 +52,8 @@ class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() { class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() {
override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

@ -29,22 +29,22 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER
import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
class MangaTypeMapping : SQLiteTypeMapping<Manga>( class MangaTypeMapping : SQLiteTypeMapping<Manga>(
MangaPutResolver(), MangaPutResolver(),
MangaGetResolver(), MangaGetResolver(),
MangaDeleteResolver() MangaDeleteResolver()
) )
class MangaPutResolver : DefaultPutResolver<Manga>() { class MangaPutResolver : DefaultPutResolver<Manga>() {
override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder() override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply { override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
@ -95,8 +95,8 @@ open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() { class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

@ -11,12 +11,14 @@ import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl 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_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ 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_LIBRARY_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_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_MEDIA_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE 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_STATUS
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE 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 import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
class TrackTypeMapping : SQLiteTypeMapping<Track>( class TrackTypeMapping : SQLiteTypeMapping<Track>(
TrackPutResolver(), TrackPutResolver(),
TrackGetResolver(), TrackGetResolver(),
TrackDeleteResolver() TrackDeleteResolver()
) )
class TrackPutResolver : DefaultPutResolver<Track>() { class TrackPutResolver : DefaultPutResolver<Track>() {
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder() override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
.table(TABLE) .table(TABLE)
.build() .build()
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: Track) = ContentValues(10).apply { override fun mapToContentValues(obj: Track) = ContentValues(10).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
@ -54,7 +56,8 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
put(COL_STATUS, obj.status) put(COL_STATUS, obj.status)
put(COL_TRACKING_URL, obj.tracking_url) put(COL_TRACKING_URL, obj.tracking_url)
put(COL_SCORE, obj.score) 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)) status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE)) score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL)) 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>() { class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder() override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
.table(TABLE) .table(TABLE)
.where("$COL_ID = ?") .where("$COL_ID = ?")
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
} }

View File

@ -23,5 +23,4 @@ interface Category : Serializable {
fun createDefault(): Category = create("Default").apply { id = 0 } fun createDefault(): Category = create("Default").apply { id = 0 }
} }
}
}

View File

@ -22,5 +22,4 @@ class CategoryImpl : Category {
override fun hashCode(): Int { override fun hashCode(): Int {
return name.hashCode() return name.hashCode()
} }
} }

View File

@ -37,5 +37,4 @@ class ChapterImpl : Chapter {
override fun hashCode(): Int { override fun hashCode(): Int {
return url.hashCode() return url.hashCode()
} }
}
}

View File

@ -35,7 +35,7 @@ interface History : Serializable {
* @param chapter chapter object * @param chapter chapter object
* @return history object * @return history object
*/ */
fun create(chapter: Chapter): History = HistoryImpl().apply { fun create(chapter: Chapter): History = HistoryImpl().apply {
this.chapter_id = chapter.id!! this.chapter_id = chapter.id!!
} }
} }

View File

@ -5,5 +5,4 @@ class LibraryManga : MangaImpl() {
var unread: Int = 0 var unread: Int = 0
var category: Int = 0 var category: Int = 0
}
}

View File

@ -28,6 +28,10 @@ interface Manga : SManga {
return chapter_flags and SORT_MASK == SORT_DESC 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 // Used to display the chapter's title one way or another
var displayMode: Int var displayMode: Int
get() = chapter_flags and DISPLAY_MASK get() = chapter_flags and DISPLAY_MASK
@ -88,5 +92,4 @@ interface Manga : SManga {
this.source = source this.source = source
} }
} }
}
}

View File

@ -17,5 +17,4 @@ class MangaCategory {
return mc return mc
} }
} }
} }

View File

@ -5,6 +5,6 @@ package eu.kanade.tachiyomi.data.database.models
* *
* @param manga object containing manga * @param manga object containing manga
* @param chapter object containing chater * @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) data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)

View File

@ -39,11 +39,9 @@ open class MangaImpl : Manga {
val manga = other as Manga val manga = other as Manga
return url == manga.url return url == manga.url
} }
override fun hashCode(): Int { override fun hashCode(): Int {
return url.hashCode() return url.hashCode()
} }
} }

View File

@ -24,12 +24,18 @@ interface Track : Serializable {
var status: Int var status: Int
var started_reading_date: Long
var finished_reading_date: Long
var tracking_url: String var tracking_url: String
fun copyPersonalFrom(other: Track) { fun copyPersonalFrom(other: Track) {
last_chapter_read = other.last_chapter_read last_chapter_read = other.last_chapter_read
score = other.score score = other.score
status = other.status status = other.status
started_reading_date = other.started_reading_date
finished_reading_date = other.finished_reading_date
} }
companion object { companion object {
@ -37,5 +43,4 @@ interface Track : Serializable {
sync_id = serviceId sync_id = serviceId
} }
} }
} }

View File

@ -22,6 +22,10 @@ class TrackImpl : Track {
override var status: Int = 0 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 var tracking_url: String = ""
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
@ -41,5 +45,4 @@ class TrackImpl : Track {
result = 31 * result + media_id result = 31 * result + media_id
return result return result
} }
} }

View File

@ -10,20 +10,24 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable
interface CategoryQueries : DbProvider { interface CategoryQueries : DbProvider {
fun getCategories() = db.get() fun getCategories() = db.get()
.listOfObjects(Category::class.java) .listOfObjects(Category::class.java)
.withQuery(Query.builder() .withQuery(
.table(CategoryTable.TABLE) Query.builder()
.orderBy(CategoryTable.COL_ORDER) .table(CategoryTable.TABLE)
.build()) .orderBy(CategoryTable.COL_ORDER)
.prepare() .build()
)
.prepare()
fun getCategoriesForManga(manga: Manga) = db.get() fun getCategoriesForManga(manga: Manga) = db.get()
.listOfObjects(Category::class.java) .listOfObjects(Category::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getCategoriesForMangaQuery()) RawQuery.builder()
.args(manga.id) .query(getCategoriesForMangaQuery())
.build()) .args(manga.id)
.prepare() .build()
)
.prepare()
fun insertCategory(category: Category) = db.put().`object`(category).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 deleteCategory(category: Category) = db.delete().`object`(category).prepare()
fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare() fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
}
}

View File

@ -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.ChapterSourceOrderPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import java.util.* import java.util.Date
interface ChapterQueries : DbProvider { interface ChapterQueries : DbProvider {
@ -27,32 +27,49 @@ interface ChapterQueries : DbProvider {
.prepare() .prepare()
fun getRecentChapters(date: Date) = db.get() fun getRecentChapters(date: Date) = db.get()
.listOfObjects(MangaChapter::class.java) .listOfObjects(MangaChapter::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getRecentsQuery()) RawQuery.builder()
.args(date.time) .query(getRecentsQuery())
.observesTables(ChapterTable.TABLE) .args(date.time)
.build()) .observesTables(ChapterTable.TABLE)
.withGetResolver(MangaChapterGetResolver.INSTANCE) .build()
.prepare() )
.withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare()
fun getChapter(id: Long) = db.get() fun getChapter(id: Long) = db.get()
.`object`(Chapter::class.java) .`object`(Chapter::class.java)
.withQuery(Query.builder() .withQuery(
.table(ChapterTable.TABLE) Query.builder()
.where("${ChapterTable.COL_ID} = ?") .table(ChapterTable.TABLE)
.whereArgs(id) .where("${ChapterTable.COL_ID} = ?")
.build()) .whereArgs(id)
.prepare() .build()
)
.prepare()
fun getChapter(url: String) = db.get() fun getChapter(url: String) = db.get()
.`object`(Chapter::class.java) .`object`(Chapter::class.java)
.withQuery(Query.builder() .withQuery(
.table(ChapterTable.TABLE) Query.builder()
.where("${ChapterTable.COL_URL} = ?") .table(ChapterTable.TABLE)
.whereArgs(url) .where("${ChapterTable.COL_URL} = ?")
.build()) .whereArgs(url)
.prepare() .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() fun getChapters(url: String) = db.get()
.listOfObjects(Chapter::class.java) .listOfObjects(Chapter::class.java)
@ -73,23 +90,22 @@ interface ChapterQueries : DbProvider {
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare() fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
fun updateChaptersBackup(chapters: List<Chapter>) = db.put() fun updateChaptersBackup(chapters: List<Chapter>) = db.put()
.objects(chapters) .objects(chapters)
.withPutResolver(ChapterBackupPutResolver()) .withPutResolver(ChapterBackupPutResolver())
.prepare() .prepare()
fun updateChapterProgress(chapter: Chapter) = db.put() fun updateChapterProgress(chapter: Chapter) = db.put()
.`object`(chapter) .`object`(chapter)
.withPutResolver(ChapterProgressPutResolver()) .withPutResolver(ChapterProgressPutResolver())
.prepare() .prepare()
fun updateChaptersProgress(chapters: List<Chapter>) = db.put() fun updateChaptersProgress(chapters: List<Chapter>) = db.put()
.objects(chapters) .objects(chapters)
.withPutResolver(ChapterProgressPutResolver()) .withPutResolver(ChapterProgressPutResolver())
.prepare() .prepare()
fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put() fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put()
.objects(chapters) .objects(chapters)
.withPutResolver(ChapterSourceOrderPutResolver()) .withPutResolver(ChapterSourceOrderPutResolver())
.prepare() .prepare()
}
}

View File

@ -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.HistoryLastReadPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
import eu.kanade.tachiyomi.data.database.tables.HistoryTable import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import java.util.* import java.util.Date
interface HistoryQueries : DbProvider { interface HistoryQueries : DbProvider {
@ -23,32 +23,38 @@ interface HistoryQueries : DbProvider {
* @param date recent date range * @param date recent date range
*/ */
fun getRecentManga(date: Date) = db.get() fun getRecentManga(date: Date) = db.get()
.listOfObjects(MangaChapterHistory::class.java) .listOfObjects(MangaChapterHistory::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getRecentMangasQuery()) RawQuery.builder()
.args(date.time) .query(getRecentMangasQuery())
.observesTables(HistoryTable.TABLE) .args(date.time)
.build()) .observesTables(HistoryTable.TABLE)
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) .build()
.prepare() )
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare()
fun getHistoryByMangaId(mangaId: Long) = db.get() fun getHistoryByMangaId(mangaId: Long) = db.get()
.listOfObjects(History::class.java) .listOfObjects(History::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getHistoryByMangaId()) RawQuery.builder()
.args(mangaId) .query(getHistoryByMangaId())
.observesTables(HistoryTable.TABLE) .args(mangaId)
.build()) .observesTables(HistoryTable.TABLE)
.prepare() .build()
)
.prepare()
fun getHistoryByChapterUrl(chapterUrl: String) = db.get() fun getHistoryByChapterUrl(chapterUrl: String) = db.get()
.`object`(History::class.java) .`object`(History::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getHistoryByChapterUrl()) RawQuery.builder()
.args(chapterUrl) .query(getHistoryByChapterUrl())
.observesTables(HistoryTable.TABLE) .args(chapterUrl)
.build()) .observesTables(HistoryTable.TABLE)
.prepare() .build()
)
.prepare()
/** /**
* Updates the history last read. * Updates the history last read.
@ -56,9 +62,9 @@ interface HistoryQueries : DbProvider {
* @param history history object * @param history history object
*/ */
fun updateHistoryLastRead(history: History) = db.put() fun updateHistoryLastRead(history: History) = db.put()
.`object`(history) .`object`(history)
.withPutResolver(HistoryLastReadPutResolver()) .withPutResolver(HistoryLastReadPutResolver())
.prepare() .prepare()
/** /**
* Updates the history last read. * Updates the history last read.
@ -66,21 +72,25 @@ interface HistoryQueries : DbProvider {
* @param historyList history object list * @param historyList history object list
*/ */
fun updateHistoryLastRead(historyList: List<History>) = db.put() fun updateHistoryLastRead(historyList: List<History>) = db.put()
.objects(historyList) .objects(historyList)
.withPutResolver(HistoryLastReadPutResolver()) .withPutResolver(HistoryLastReadPutResolver())
.prepare() .prepare()
fun deleteHistory() = db.delete() fun deleteHistory() = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
.table(HistoryTable.TABLE) DeleteQuery.builder()
.build()) .table(HistoryTable.TABLE)
.prepare() .build()
)
.prepare()
fun deleteHistoryNoLastRead() = db.delete() fun deleteHistoryNoLastRead() = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
.table(HistoryTable.TABLE) DeleteQuery.builder()
.where("${HistoryTable.COL_LAST_READ} = ?") .table(HistoryTable.TABLE)
.whereArgs(0) .where("${HistoryTable.COL_LAST_READ} = ?")
.build()) .whereArgs(0)
.prepare() .build()
)
.prepare()
} }

View File

@ -15,12 +15,14 @@ interface MangaCategoryQueries : DbProvider {
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare() fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete() fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
.table(MangaCategoryTable.TABLE) DeleteQuery.builder()
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})") .table(MangaCategoryTable.TABLE)
.whereArgs(*mangas.map { it.id }.toTypedArray()) .where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
.build()) .whereArgs(*mangas.map { it.id }.toTypedArray())
.prepare() .build()
)
.prepare()
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) { fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
db.inTransaction { db.inTransaction {
@ -32,5 +34,4 @@ interface MangaCategoryQueries : DbProvider {
} }
} }
} }
}
}

View File

@ -6,7 +6,12 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga 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.CategoryTable
import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
@ -16,107 +21,140 @@ import exh.metadata.sql.tables.SearchMetadataTable
interface MangaQueries : DbProvider { interface MangaQueries : DbProvider {
fun getMangas() = db.get() fun getMangas() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(Query.builder() .withQuery(
.table(MangaTable.TABLE) Query.builder()
.build()) .table(MangaTable.TABLE)
.prepare() .build()
)
.prepare()
fun getLibraryMangas() = db.get() fun getLibraryMangas() = db.get()
.listOfObjects(LibraryManga::class.java) .listOfObjects(LibraryManga::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(libraryQuery) RawQuery.builder()
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE) .query(libraryQuery)
.build()) .observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
.withGetResolver(LibraryMangaGetResolver.INSTANCE) .build()
.prepare() )
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare()
fun getFavoriteMangas() = db.get() fun getFavoriteMangas() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(Query.builder() .withQuery(
.table(MangaTable.TABLE) Query.builder()
.where("${MangaTable.COL_FAVORITE} = ?") .table(MangaTable.TABLE)
.whereArgs(1) .where("${MangaTable.COL_FAVORITE} = ?")
.orderBy(MangaTable.COL_TITLE) .whereArgs(1)
.build()) .orderBy(MangaTable.COL_TITLE)
.prepare() .build()
)
.prepare()
fun getManga(url: String, sourceId: Long) = db.get() fun getManga(url: String, sourceId: Long) = db.get()
.`object`(Manga::class.java) .`object`(Manga::class.java)
.withQuery(Query.builder() .withQuery(
.table(MangaTable.TABLE) Query.builder()
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?") .table(MangaTable.TABLE)
.whereArgs(url, sourceId) .where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
.build()) .whereArgs(url, sourceId)
.prepare() .build()
)
.prepare()
fun getManga(id: Long) = db.get() fun getManga(id: Long) = db.get()
.`object`(Manga::class.java) .`object`(Manga::class.java)
.withQuery(Query.builder() .withQuery(
.table(MangaTable.TABLE) Query.builder()
.where("${MangaTable.COL_ID} = ?") .table(MangaTable.TABLE)
.whereArgs(id) .where("${MangaTable.COL_ID} = ?")
.build()) .whereArgs(id)
.prepare() .build()
)
.prepare()
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare() fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare() fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
fun updateFlags(manga: Manga) = db.put() fun updateFlags(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaFlagsPutResolver()) .withPutResolver(MangaFlagsPutResolver())
.prepare() .prepare()
fun updateLastUpdated(manga: Manga) = db.put() fun updateLastUpdated(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaLastUpdatedPutResolver()) .withPutResolver(MangaLastUpdatedPutResolver())
.prepare() .prepare()
fun updateMangaFavorite(manga: Manga) = db.put() fun updateMangaFavorite(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaFavoritePutResolver()) .withPutResolver(MangaFavoritePutResolver())
.prepare() .prepare()
fun updateMangaViewer(manga: Manga) = db.put() fun updateMangaViewer(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaViewerPutResolver()) .withPutResolver(MangaViewerPutResolver())
.prepare() .prepare()
fun updateMangaTitle(manga: Manga) = db.put() fun updateMangaTitle(manga: Manga) = db.put()
.`object`(manga) .`object`(manga)
.withPutResolver(MangaTitlePutResolver()) .withPutResolver(MangaTitlePutResolver())
.prepare() .prepare()
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare() fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
fun deleteMangasNotInLibrary() = db.delete() fun deleteMangasNotInLibrary() = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
.table(MangaTable.TABLE) DeleteQuery.builder()
.where("${MangaTable.COL_FAVORITE} = ?") .table(MangaTable.TABLE)
.whereArgs(0) .where("${MangaTable.COL_FAVORITE} = ?")
.build()) .whereArgs(0)
.prepare() .build()
)
.prepare()
fun deleteMangas() = db.delete() fun deleteMangas() = db.delete()
.byQuery(DeleteQuery.builder() .byQuery(
.table(MangaTable.TABLE) DeleteQuery.builder()
.build()) .table(MangaTable.TABLE)
.prepare() .build()
)
.prepare()
fun getLastReadManga() = db.get() fun getLastReadManga() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder() .withQuery(
.query(getLastReadMangaQuery()) RawQuery.builder()
.observesTables(MangaTable.TABLE) .query(getLastReadMangaQuery())
.build()) .observesTables(MangaTable.TABLE)
.prepare() .build()
)
.prepare()
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java) fun getTotalChapterManga() = db.get()
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare(); .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() fun getMangaWithMetadata() = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder() .withQuery(RawQuery.builder()

View File

@ -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. * 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} SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
FROM ( FROM (
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD} 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. * 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} 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} ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ? WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
@ -47,7 +49,8 @@ fun getRecentsQuery() = """
* and are read after the given time period * and are read after the given time period
* @return return limit is 25 * @return return limit is 25
*/ */
fun getRecentMangasQuery() = """ fun getRecentMangasQuery() =
"""
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.* SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE} JOIN ${Chapter.TABLE}
@ -65,7 +68,8 @@ fun getRecentMangasQuery() = """
LIMIT 25 LIMIT 25
""" """
fun getHistoryByMangaId() = """ fun getHistoryByMangaId() =
"""
SELECT ${History.TABLE}.* SELECT ${History.TABLE}.*
FROM ${History.TABLE} FROM ${History.TABLE}
JOIN ${Chapter.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} 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}.* SELECT ${History.TABLE}.*
FROM ${History.TABLE} FROM ${History.TABLE}
JOIN ${Chapter.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} 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 SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE} JOIN ${Chapter.TABLE}
@ -93,7 +99,8 @@ fun getLastReadMangaQuery() = """
ORDER BY max DESC ORDER BY max DESC
""" """
fun getTotalChapterMangaQuery()= """ fun getTotalChapterMangaQuery() =
"""
SELECT ${Manga.TABLE}.* SELECT ${Manga.TABLE}.*
FROM ${Manga.TABLE} FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE} JOIN ${Chapter.TABLE}
@ -102,12 +109,23 @@ fun getTotalChapterMangaQuery()= """
ORDER by COUNT(*) 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. * Query to get the categories for a manga.
*/ */
fun getCategoriesForMangaQuery() = """ fun getCategoriesForMangaQuery() =
"""
SELECT ${Category.TABLE}.* FROM ${Category.TABLE} SELECT ${Category.TABLE}.* FROM ${Category.TABLE}
JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} = JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} =
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID} ${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
WHERE ${MangaCategory.COL_MANGA_ID} = ? WHERE ${MangaCategory.COL_MANGA_ID} = ?
""" """

View File

@ -1,34 +1,37 @@
package eu.kanade.tachiyomi.data.database.queries package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.tables.TrackTable import eu.kanade.tachiyomi.data.database.tables.TrackTable
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
interface TrackQueries : DbProvider { interface TrackQueries : DbProvider {
fun getTracks(manga: Manga) = db.get() fun getTracks(manga: Manga) = db.get()
.listOfObjects(Track::class.java) .listOfObjects(Track::class.java)
.withQuery(Query.builder() .withQuery(
.table(TrackTable.TABLE) Query.builder()
.where("${TrackTable.COL_MANGA_ID} = ?") .table(TrackTable.TABLE)
.whereArgs(manga.id) .where("${TrackTable.COL_MANGA_ID} = ?")
.build()) .whereArgs(manga.id)
.prepare() .build()
)
fun insertTrack(track: Track) = db.put().`object`(track).prepare() .prepare()
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare() fun insertTrack(track: Track) = db.put().`object`(track).prepare()
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete() fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
.byQuery(DeleteQuery.builder()
.table(TrackTable.TABLE) fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?") .byQuery(
.whereArgs(manga.id, sync.id) DeleteQuery.builder()
.build()) .table(TrackTable.TABLE)
.prepare() .where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
.whereArgs(manga.id, sync.id)
} .build()
)
.prepare()
}

View File

@ -20,16 +20,14 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
} }
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?") .where("${ChapterTable.COL_URL} = ?")
.whereArgs(chapter.url) .whereArgs(chapter.url)
.build() .build()
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply { fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
put(ChapterTable.COL_READ, chapter.read) put(ChapterTable.COL_READ, chapter.read)
put(ChapterTable.COL_BOOKMARK, chapter.bookmark) put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read) put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
} }
} }

View File

@ -20,16 +20,14 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
} }
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?") .where("${ChapterTable.COL_ID} = ?")
.whereArgs(chapter.id) .whereArgs(chapter.id)
.build() .build()
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply { fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
put(ChapterTable.COL_READ, chapter.read) put(ChapterTable.COL_READ, chapter.read)
put(ChapterTable.COL_BOOKMARK, chapter.bookmark) put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read) put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
} }
} }

View File

@ -20,13 +20,12 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
} }
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder() fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE) .table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?") .where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(chapter.url, chapter.manga_id) .whereArgs(chapter.url, chapter.manga_id)
.build() .build()
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply { fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order) put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
} }
}
}

View File

@ -19,25 +19,25 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn { override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(history) val updateQuery = mapToUpdateQuery(history)
val cursor = db.lowLevel().query(Query.builder() val cursor = db.lowLevel().query(
Query.builder()
.table(updateQuery.table()) .table(updateQuery.table())
.where(updateQuery.where()) .where(updateQuery.where())
.whereArgs(updateQuery.whereArgs()) .whereArgs(updateQuery.whereArgs())
.build()) .build()
)
val putResult: PutResult val putResult: PutResult
try { putResult = cursor.use { putCursor ->
if (cursor.count == 0) { if (putCursor.count == 0) {
val insertQuery = mapToInsertQuery(history) val insertQuery = mapToInsertQuery(history)
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history)) val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
putResult = PutResult.newInsertResult(insertedId, insertQuery.table()) PutResult.newInsertResult(insertedId, insertQuery.table())
} else { } else {
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history)) val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history))
putResult = PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table()) PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
} }
} finally {
cursor.close()
} }
putResult putResult
@ -48,10 +48,10 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
* @param obj history object * @param obj history object
*/ */
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder() override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
.table(HistoryTable.TABLE) .table(HistoryTable.TABLE)
.where("${HistoryTable.COL_CHAPTER_ID} = ?") .where("${HistoryTable.COL_CHAPTER_ID} = ?")
.whereArgs(obj.chapter_id) .whereArgs(obj.chapter_id)
.build() .build()
/** /**
* Create content query * Create content query
@ -60,5 +60,4 @@ class HistoryLastReadPutResolver : HistoryPutResolver() {
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply { fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
put(HistoryTable.COL_LAST_READ, history.last_read) put(HistoryTable.COL_LAST_READ, history.last_read)
} }
} }

View File

@ -21,5 +21,4 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
return manga return manga
} }
} }

View File

@ -24,5 +24,4 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
return MangaChapter(manga, chapter) return MangaChapter(manga, chapter)
} }
} }

View File

@ -20,14 +20,12 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
} }
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build() .build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_FAVORITE, manga.favorite) put(MangaTable.COL_FAVORITE, manga.favorite)
} }
} }

View File

@ -20,14 +20,12 @@ class MangaFlagsPutResolver : PutResolver<Manga>() {
} }
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build() .build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags) put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags)
} }
} }

View File

@ -20,14 +20,12 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
} }
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build() .build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_LAST_UPDATE, manga.last_update) put(MangaTable.COL_LAST_UPDATE, manga.last_update)
} }
} }

View File

@ -20,13 +20,12 @@ class MangaTitlePutResolver : PutResolver<Manga>() {
} }
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build() .build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_TITLE, manga.title) put(MangaTable.COL_TITLE, manga.title)
} }
} }

View File

@ -20,13 +20,12 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
} }
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build() .build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_VIEWER, manga.viewer) put(MangaTable.COL_VIEWER, manga.viewer)
} }
} }

View File

@ -13,11 +13,11 @@ object CategoryTable {
const val COL_FLAGS = "flags" const val COL_FLAGS = "flags"
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_NAME TEXT NOT NULL, $COL_NAME TEXT NOT NULL,
$COL_ORDER INTEGER NOT NULL, $COL_ORDER INTEGER NOT NULL,
$COL_FLAGS INTEGER NOT NULL $COL_FLAGS INTEGER NOT NULL
)""" )"""
} }

View File

@ -29,7 +29,8 @@ object ChapterTable {
const val COL_SOURCE_ORDER = "source_order" const val COL_SOURCE_ORDER = "source_order"
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL, $COL_MANGA_ID INTEGER NOT NULL,
$COL_URL TEXT NOT NULL, $COL_URL TEXT NOT NULL,
@ -51,7 +52,7 @@ object ChapterTable {
val createUnreadChaptersIndexQuery: String val createUnreadChaptersIndexQuery: String
get() = "CREATE INDEX ${TABLE}_unread_by_manga_index ON $TABLE($COL_MANGA_ID, $COL_READ) " + 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 val sourceOrderUpdateQuery: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0" get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0"
@ -61,5 +62,4 @@ object ChapterTable {
val addScanlator: String val addScanlator: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL" get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
} }

View File

@ -31,7 +31,8 @@ object HistoryTable {
* query to create history table * query to create history table
*/ */
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_CHAPTER_ID INTEGER NOT NULL UNIQUE, $COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
$COL_LAST_READ LONG, $COL_LAST_READ LONG,

View File

@ -11,7 +11,8 @@ object MangaCategoryTable {
const val COL_CATEGORY_ID = "category_id" const val COL_CATEGORY_ID = "category_id"
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL, $COL_MANGA_ID INTEGER NOT NULL,
$COL_CATEGORY_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}) FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
ON DELETE CASCADE ON DELETE CASCADE
)""" )"""
} }

View File

@ -39,7 +39,8 @@ object MangaTable {
const val COL_CATEGORY = "category" const val COL_CATEGORY = "category"
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_SOURCE INTEGER NOT NULL, $COL_SOURCE INTEGER NOT NULL,
$COL_URL TEXT NOT NULL, $COL_URL TEXT NOT NULL,
@ -62,5 +63,5 @@ object MangaTable {
val createLibraryIndexQuery: String val createLibraryIndexQuery: String
get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " + get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " +
"WHERE $COL_FAVORITE = 1" "WHERE $COL_FAVORITE = 1"
} }

View File

@ -26,8 +26,13 @@ object TrackTable {
const val COL_TRACKING_URL = "remote_url" const val COL_TRACKING_URL = "remote_url"
const val COL_START_DATE = "start_date"
const val COL_FINISH_DATE = "finish_date"
val createTableQuery: String val createTableQuery: String
get() = """CREATE TABLE $TABLE( get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY, $COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL, $COL_MANGA_ID INTEGER NOT NULL,
$COL_SYNC_ID INTEGER NOT NULL, $COL_SYNC_ID INTEGER NOT NULL,
@ -39,6 +44,8 @@ object TrackTable {
$COL_STATUS INTEGER NOT NULL, $COL_STATUS INTEGER NOT NULL,
$COL_SCORE FLOAT NOT NULL, $COL_SCORE FLOAT NOT NULL,
$COL_TRACKING_URL TEXT 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, UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE,
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
ON DELETE CASCADE ON DELETE CASCADE
@ -49,4 +56,10 @@ object TrackTable {
val addLibraryId: String val addLibraryId: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL" 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"
} }

View File

@ -6,11 +6,11 @@ import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.SourceManager 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.Injekt
import uy.kohesive.injekt.api.get 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 * 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. * @param preferences the preferences of the app.
*/ */
class DownloadCache( class DownloadCache(
private val context: Context, private val context: Context,
private val provider: DownloadProvider, private val provider: DownloadProvider,
private val sourceManager: SourceManager, private val sourceManager: SourceManager,
private val preferences: PreferencesHelper = Injekt.get() private val preferences: PreferencesHelper = Injekt.get()
) { ) {
/** /**
@ -47,19 +47,18 @@ class DownloadCache(
private var rootDir = RootDirectory(getDirectoryFromPreference()) private var rootDir = RootDirectory(getDirectoryFromPreference())
init { init {
preferences.downloadsDirectory().asObservable() preferences.downloadsDirectory().asFlow()
.skip(1) .onEach {
.subscribe { lastRenew = 0L // invalidate cache
lastRenew = 0L // invalidate cache rootDir = RootDirectory(getDirectoryFromPreference())
rootDir = RootDirectory(getDirectoryFromPreference()) }
}
} }
/** /**
* Returns the downloads directory from the user's preferences. * Returns the downloads directory from the user's preferences.
*/ */
private fun getDirectoryFromPreference(): UniFile { private fun getDirectoryFromPreference(): UniFile {
val dir = preferences.downloadsDirectory().getOrDefault() val dir = preferences.downloadsDirectory().get()
return UniFile.fromUri(context, Uri.parse(dir)) return UniFile.fromUri(context, Uri.parse(dir))
} }
@ -100,7 +99,9 @@ class DownloadCache(
if (sourceDir != null) { if (sourceDir != null) {
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
if (mangaDir != null) { if (mangaDir != null) {
return mangaDir.files.size return mangaDir.files
.filter { !it.endsWith(Downloader.TMP_DIR_SUFFIX) }
.size
} }
} }
return 0 return 0
@ -124,26 +125,26 @@ class DownloadCache(
val onlineSources = sourceManager.getOnlineSources() val onlineSources = sourceManager.getOnlineSources()
val sourceDirs = rootDir.dir.listFiles() val sourceDirs = rootDir.dir.listFiles()
.orEmpty() .orEmpty()
.associate { it.name to SourceDirectory(it) } .associate { it.name to SourceDirectory(it) }
.mapNotNullKeys { entry -> .mapNotNullKeys { entry ->
onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id
} }
rootDir.files = sourceDirs rootDir.files = sourceDirs
sourceDirs.values.forEach { sourceDir -> sourceDirs.values.forEach { sourceDir ->
val mangaDirs = sourceDir.dir.listFiles() val mangaDirs = sourceDir.dir.listFiles()
.orEmpty() .orEmpty()
.associateNotNullKeys { it.name to MangaDirectory(it) } .associateNotNullKeys { it.name to MangaDirectory(it) }
sourceDir.files = mangaDirs sourceDir.files = mangaDirs
mangaDirs.values.forEach { mangaDir -> mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir.listFiles() val chapterDirs = mangaDir.dir.listFiles()
.orEmpty() .orEmpty()
.mapNotNull { it.name } .mapNotNull { it.name }
.toHashSet() .toHashSet()
mangaDir.files = chapterDirs mangaDir.files = chapterDirs
} }
@ -231,27 +232,33 @@ class DownloadCache(
/** /**
* Class to store the files under the root downloads directory. * Class to store the files under the root downloads directory.
*/ */
private class RootDirectory(val dir: UniFile, private class RootDirectory(
var files: Map<Long, SourceDirectory> = hashMapOf()) val dir: UniFile,
var files: Map<Long, SourceDirectory> = hashMapOf()
)
/** /**
* Class to store the files under a source directory. * Class to store the files under a source directory.
*/ */
private class SourceDirectory(val dir: UniFile, private class SourceDirectory(
var files: Map<String, MangaDirectory> = hashMapOf()) val dir: UniFile,
var files: Map<String, MangaDirectory> = hashMapOf()
)
/** /**
* Class to store the files under a manga directory. * Class to store the files under a manga directory.
*/ */
private class MangaDirectory(val dir: UniFile, private class MangaDirectory(
var files: Set<String> = hashSetOf()) val dir: UniFile,
var files: Set<String> = hashSetOf()
)
/** /**
* Returns a new map containing only the key entries of [transform] that are not null. * 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> { 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>() 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 return destination
} }
@ -263,10 +270,9 @@ class DownloadCache(
for (element in this) { for (element in this) {
val (key, value) = transform(element) val (key, value) = transform(element)
if (key != null) { if (key != null) {
destination.put(key, value) destination[key] = value
} }
} }
return destination return destination
} }
} }

View File

@ -20,7 +20,7 @@ import uy.kohesive.injekt.injectLazy
* *
* @param context the application context. * @param context the application context.
*/ */
class DownloadManager(context: Context) { class DownloadManager(private val context: Context) {
/** /**
* The sources manager. * The sources manager.
@ -99,10 +99,21 @@ class DownloadManager(context: Context) {
* @param downloads value to set the download queue to * @param downloads value to set the download queue to
*/ */
fun reorderQueue(downloads: List<Download>) { fun reorderQueue(downloads: List<Download>) {
val wasRunning = downloader.isRunning
if (downloads.isEmpty()) {
DownloadService.stop(context)
downloader.queue.clear()
return
}
downloader.pause() downloader.pause()
downloader.queue.clear() downloader.queue.clear()
downloader.queue.addAll(downloads) 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>> { private fun buildPageList(chapterDir: UniFile?): Observable<List<Page>> {
return Observable.fromCallable { return Observable.fromCallable {
val files = chapterDir?.listFiles().orEmpty() val files = chapterDir?.listFiles().orEmpty()
.filter { "image" in it.type.orEmpty() } .filter { "image" in it.type.orEmpty() }
if (files.isEmpty()) { if (files.isEmpty()) {
throw Exception("Page list is empty") throw Exception("Page list is empty")
} }
files.sortedBy { it.name } files.sortedBy { it.name }
.mapIndexed { i, file -> .mapIndexed { i, file ->
Page(i, uri = file.uri).apply { status = Page.READY } Page(i, uri = file.uri).apply { status = Page.READY }
} }
} }
} }
@ -170,6 +181,15 @@ class DownloadManager(context: Context) {
return cache.getDownloadCount(manga) 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. * Deletes the directories of a list of downloaded chapters.
* *
@ -219,5 +239,4 @@ class DownloadManager(context: Context) {
deleteChapters(chapters, manga, source) deleteChapters(chapters, manga, source)
} }
} }
} }

View File

@ -5,13 +5,16 @@ import android.graphics.BitmapFactory
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download 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.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.chop import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.notificationManager 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 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. * 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 * @param context context of application
*/ */
internal class DownloadNotifier(private val context: Context) { internal class DownloadNotifier(private val context: Context) {
/**
* Notification builder. private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) {
*/ setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
private val notification by lazy {
NotificationCompat.Builder(context, 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. * Status of download. Used for correct notification icon.
*/ */
private var isDownloading = false 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 * Updated when error is thrown
*/ */
var errorThrown = false var errorThrown = false
/**
* Updated when only single page is downloaded
*/
var isSingleChapter = false
/** /**
* Updated when paused * Updated when paused
*/ */
@ -70,9 +56,10 @@ internal class DownloadNotifier(private val context: Context) {
/** /**
* Clear old actions if they exist. * Clear old actions if they exist.
*/ */
private fun clearActions() = with(notification) { private fun clearActions() = with(notificationBuilder) {
if (!mActions.isEmpty()) if (mActions.isNotEmpty()) {
mActions.clear() mActions.clear()
}
} }
/** /**
@ -90,7 +77,7 @@ internal class DownloadNotifier(private val context: Context) {
*/ */
fun onProgressChange(download: Download) { fun onProgressChange(download: Download) {
// Create notification // Create notification
with(notification) { with(notificationBuilder) {
// Check if first call. // Check if first call.
if (!isDownloading) { if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download) setSmallIcon(android.R.drawable.stat_sys_download)
@ -100,84 +87,65 @@ internal class DownloadNotifier(private val context: Context) {
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
isDownloading = true isDownloading = true
// Pause action // Pause action
addAction(R.drawable.ic_av_pause_grey_24dp_img, addAction(
context.getString(R.string.action_pause), R.drawable.ic_pause_24dp,
NotificationReceiver.pauseDownloadsPendingBroadcast(context)) 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) setProgress(download.pages!!.size, download.downloadedImages, false)
} }
// Displays the progress bar on notification // Displays the progress bar on notification
notification.show() notificationBuilder.show()
} }
/** /**
* Show notification when download is paused. * Show notification when download is paused.
*/ */
fun onDownloadPaused() { fun onDownloadPaused() {
with(notification) { with(notificationBuilder) {
setContentTitle(context.getString(R.string.chapter_paused)) setContentTitle(context.getString(R.string.chapter_paused))
setContentText(context.getString(R.string.download_notifier_download_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) setAutoCancel(false)
setProgress(0, 0, false) setProgress(0, 0, false)
clearActions() clearActions()
// Open download manager when clicked // Open download manager when clicked
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
// Resume action // Resume action
addAction(R.drawable.ic_av_play_arrow_grey_img, addAction(
context.getString(R.string.action_resume), R.drawable.ic_play_arrow_24dp,
NotificationReceiver.resumeDownloadsPendingBroadcast(context)) context.getString(R.string.action_resume),
//Clear action NotificationReceiver.resumeDownloadsPendingBroadcast(context)
addAction(R.drawable.ic_clear_grey_24dp_img, )
context.getString(R.string.action_clear), // Clear action
NotificationReceiver.clearDownloadsPendingBroadcast(context)) addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.action_cancel_all),
NotificationReceiver.clearDownloadsPendingBroadcast(context)
)
} }
// Show notification. // Show notification.
notification.show() notificationBuilder.show()
// Reset initial values // Reset initial values
isDownloading = false 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. * @param reason the text to show.
*/ */
fun onWarning(reason: String) { fun onWarning(reason: String) {
with(notification) { with(notificationBuilder) {
setContentTitle(context.getString(R.string.download_notifier_downloader_title)) setContentTitle(context.getString(R.string.download_notifier_downloader_title))
setContentText(reason) setContentText(reason)
setSmallIcon(android.R.drawable.stat_sys_warning) setSmallIcon(android.R.drawable.stat_sys_warning)
@ -195,7 +163,7 @@ internal class DownloadNotifier(private val context: Context) {
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false) setProgress(0, 0, false)
} }
notification.show() notificationBuilder.show()
// Reset download information // Reset download information
isDownloading = false isDownloading = false
@ -210,16 +178,19 @@ internal class DownloadNotifier(private val context: Context) {
*/ */
fun onError(error: String? = null, chapter: String? = null) { fun onError(error: String? = null, chapter: String? = null) {
// Create notification // Create notification
with(notification) { with(notificationBuilder) {
setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title)) setContentTitle(
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error)) 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) setSmallIcon(android.R.drawable.stat_sys_warning)
clearActions() clearActions()
setAutoCancel(false) setAutoCancel(false)
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false) setProgress(0, 0, false)
} }
notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR) notificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
// Reset download information // Reset download information
errorThrown = true errorThrown = true

View File

@ -120,27 +120,27 @@ class DownloadPendingDeleter(context: Context) {
* Class used to save an entry of chapters with their manga into preferences. * Class used to save an entry of chapters with their manga into preferences.
*/ */
private data class Entry( private data class Entry(
val chapters: List<ChapterEntry>, val chapters: List<ChapterEntry>,
val manga: MangaEntry val manga: MangaEntry
) )
/** /**
* Class used to save an entry for a chapter into preferences. * Class used to save an entry for a chapter into preferences.
*/ */
private data class ChapterEntry( private data class ChapterEntry(
val id: Long, val id: Long,
val url: String, val url: String,
val name: String val name: String
) )
/** /**
* Class used to save an entry for a manga into preferences. * Class used to save an entry for a manga into preferences.
*/ */
private data class MangaEntry( private data class MangaEntry(
val id: Long, val id: Long,
val url: String, val url: String,
val title: String, val title: String,
val source: Long val source: Long
) )
/** /**
@ -176,5 +176,4 @@ class DownloadPendingDeleter(context: Context) {
it.name = name it.name = name
} }
} }
} }

View File

@ -3,12 +3,17 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.hippo.unifile.UniFile 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.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.Source 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 import uy.kohesive.injekt.injectLazy
/** /**
@ -19,24 +24,23 @@ import uy.kohesive.injekt.injectLazy
*/ */
class DownloadProvider(private val context: Context) { class DownloadProvider(private val context: Context) {
/**
* Preferences helper.
*/
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private val scope = CoroutineScope(Job() + Dispatchers.Main)
/** /**
* The root directory for downloads. * 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)) val dir = UniFile.fromUri(context, Uri.parse(it))
DiskUtil.createNoMediaFile(dir, context) DiskUtil.createNoMediaFile(dir, context)
dir dir
} }
init { init {
preferences.downloadsDirectory().asObservable() preferences.downloadsDirectory().asFlow()
.skip(1) .onEach { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
.subscribe { 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. * @param source the source of the manga.
*/ */
internal fun getMangaDir(manga: Manga, source: Source): UniFile { internal fun getMangaDir(manga: Manga, source: Source): UniFile {
return downloadsDir try {
return downloadsDir
.createDirectory(getSourceDirName(source)) .createDirectory(getSourceDirName(source))
.createDirectory(getMangaDirName(manga)) .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 { fun getChapterDirName(chapter: Chapter): String {
return DiskUtil.buildValidFilename(chapter.name) return DiskUtil.buildValidFilename(chapter.name)
} }
} }

View File

@ -9,17 +9,17 @@ import android.net.NetworkInfo.State.DISCONNECTED
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import androidx.core.app.NotificationCompat
import com.github.pwittchen.reactivenetwork.library.Connectivity import com.github.pwittchen.reactivenetwork.library.Connectivity
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.connectivityManager import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.plusAssign import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.powerManager import eu.kanade.tachiyomi.util.system.notification
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.toast
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
@ -63,14 +63,8 @@ class DownloadService : Service() {
} }
} }
/**
* Download manager.
*/
private val downloadManager: DownloadManager by injectLazy() private val downloadManager: DownloadManager by injectLazy()
/**
* Preferences helper.
*/
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
/** /**
@ -112,7 +106,7 @@ class DownloadService : Service() {
* Not used. * Not used.
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 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() { private fun listenNetworkChanges() {
subscriptions += ReactiveNetwork.observeNetworkConnectivity(applicationContext) subscriptions += ReactiveNetwork.observeNetworkConnectivity(applicationContext)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ state -> onNetworkStateChanged(state) .subscribe(
}, { { state ->
onNetworkStateChanged(state)
},
{
toast(R.string.download_queue_error) toast(R.string.download_queue_error)
stopSelf() stopSelf()
}) }
)
} }
/** /**
@ -156,7 +154,9 @@ class DownloadService : Service() {
DISCONNECTED -> { DISCONNECTED -> {
downloadManager.stopDownloads(getString(R.string.download_notifier_no_network)) 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() { private fun listenDownloaderState() {
subscriptions += downloadManager.runningRelay.subscribe { running -> subscriptions += downloadManager.runningRelay.subscribe { running ->
if (running) if (running) {
wakeLock.acquireIfNeeded() wakeLock.acquireIfNeeded()
else } else {
wakeLock.releaseIfNeeded() wakeLock.releaseIfNeeded()
}
} }
} }
@ -187,9 +188,8 @@ class DownloadService : Service() {
} }
private fun getPlaceholderNotification(): Notification { private fun getPlaceholderNotification(): Notification {
return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER) return notification(Notifications.CHANNEL_DOWNLOADER) {
.setContentTitle(getString(R.string.download_notifier_downloader_title)) setContentTitle(getString(R.string.download_notifier_downloader_title))
.build() }
} }
} }

View File

@ -15,8 +15,8 @@ import uy.kohesive.injekt.injectLazy
* @param context the application context. * @param context the application context.
*/ */
class DownloadStore( class DownloadStore(
context: Context, context: Context,
private val sourceManager: SourceManager private val sourceManager: SourceManager
) { ) {
/** /**
@ -29,9 +29,6 @@ class DownloadStore(
*/ */
private val gson: Gson by injectLazy() private val gson: Gson by injectLazy()
/**
* Database helper.
*/
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
/** /**
@ -80,9 +77,9 @@ class DownloadStore(
*/ */
fun restore(): List<Download> { fun restore(): List<Download> {
val objs = preferences.all val objs = preferences.all
.mapNotNull { it.value as? String } .mapNotNull { it.value as? String }
.mapNotNull { deserialize(it) } .mapNotNull { deserialize(it) }
.sortedBy { it.order } .sortedBy { it.order }
val downloads = mutableListOf<Download>() val downloads = mutableListOf<Download>()
if (objs.isNotEmpty()) { if (objs.isNotEmpty()) {
@ -133,5 +130,4 @@ class DownloadStore(
* @param order the order of the download in the queue. * @param order the order of the download in the queue.
*/ */
data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int) data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int)
} }

View File

@ -6,6 +6,7 @@ import com.elvishew.xlog.XLog
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay 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.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download 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.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList 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 kotlinx.coroutines.async
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
@ -22,6 +30,7 @@ import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy
/** /**
* This class is the one in charge of downloading chapters. * This class is the one in charge of downloading chapters.
@ -38,12 +47,14 @@ import timber.log.Timber
* @param sourceManager the source manager. * @param sourceManager the source manager.
*/ */
class Downloader( class Downloader(
private val context: Context, private val context: Context,
private val provider: DownloadProvider, private val provider: DownloadProvider,
private val cache: DownloadCache, private val cache: DownloadCache,
private val sourceManager: SourceManager private val sourceManager: SourceManager
) { ) {
private val chapterCache: ChapterCache by injectLazy()
/** /**
* Store for persisting downloads across restarts. * Store for persisting downloads across restarts.
*/ */
@ -77,7 +88,9 @@ class Downloader(
/** /**
* Whether the downloader is running. * Whether the downloader is running.
*/ */
@Volatile private var isRunning: Boolean = false @Volatile
var isRunning: Boolean = false
private set
init { init {
launchNow { launchNow {
@ -93,17 +106,19 @@ class Downloader(
* @return true if the downloader is started, false otherwise. * @return true if the downloader is started, false otherwise.
*/ */
fun start(): Boolean { fun start(): Boolean {
if (isRunning || queue.isEmpty()) if (isRunning || queue.isEmpty()) {
return false return false
notifier.paused = false }
if (!subscriptions.hasSubscriptions())
if (!subscriptions.hasSubscriptions()) {
initializeSubscriptions() initializeSubscriptions()
}
val pending = queue.filter { it.status != Download.DOWNLOADED } val pending = queue.filter { it.status != Download.DOWNLOADED }
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE } pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
downloadsRelay.call(pending) downloadsRelay.call(pending)
return !pending.isEmpty() return pending.isNotEmpty()
} }
/** /**
@ -112,8 +127,8 @@ class Downloader(
fun stop(reason: String? = null) { fun stop(reason: String? = null) {
destroySubscriptions() destroySubscriptions()
queue queue
.filter { it.status == Download.DOWNLOADING } .filter { it.status == Download.DOWNLOADING }
.forEach { it.status = Download.ERROR } .forEach { it.status = Download.ERROR }
if (reason != null) { if (reason != null) {
notifier.onWarning(reason) notifier.onWarning(reason)
@ -121,8 +136,6 @@ class Downloader(
if (notifier.paused) { if (notifier.paused) {
notifier.paused = false notifier.paused = false
notifier.onDownloadPaused() notifier.onDownloadPaused()
} else if (notifier.isSingleChapter && !notifier.errorThrown) {
notifier.isSingleChapter = false
} else { } else {
notifier.dismiss() notifier.dismiss()
} }
@ -135,8 +148,8 @@ class Downloader(
fun pause() { fun pause() {
destroySubscriptions() destroySubscriptions()
queue queue
.filter { it.status == Download.DOWNLOADING } .filter { it.status == Download.DOWNLOADING }
.forEach { it.status = Download.QUEUE } .forEach { it.status = Download.QUEUE }
notifier.paused = true notifier.paused = true
} }
@ -148,11 +161,11 @@ class Downloader(
fun clearQueue(isNotification: Boolean = false) { fun clearQueue(isNotification: Boolean = false) {
destroySubscriptions() destroySubscriptions()
//Needed to update the chapter view // Needed to update the chapter view
if (isNotification) { if (isNotification) {
queue queue
.filter { it.status == Download.QUEUE } .filter { it.status == Download.QUEUE }
.forEach { it.status = Download.NOT_DOWNLOADED } .forEach { it.status = Download.NOT_DOWNLOADED }
} }
queue.clear() queue.clear()
notifier.dismiss() notifier.dismiss()
@ -169,15 +182,19 @@ class Downloader(
subscriptions.clear() subscriptions.clear()
subscriptions += downloadsRelay.concatMapIterable { it } subscriptions += downloadsRelay.concatMapIterable { it }
.concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) } .concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) }
.onBackpressureBuffer() .onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ completeDownload(it) .subscribe(
}, { error -> {
completeDownload(it)
},
{ error ->
DownloadService.stop(context) DownloadService.stop(context)
Timber.e(error) Timber.e(error)
notifier.onError(error.message) notifier.onError(error.message)
}) }
)
} }
/** /**
@ -200,40 +217,37 @@ class Downloader(
*/ */
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI { fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
val source = sourceManager.get(manga.source) as? HttpSource ?: return@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. // Called in background thread, the operation can be slow with SAF.
val chaptersWithoutDir = async { val chaptersWithoutDir = async {
val mangaDir = provider.findMangaDir(manga, source) val mangaDir = provider.findMangaDir(manga, source)
chapters chapters
// Avoid downloading chapters with the same name. // Avoid downloading chapters with the same name.
.distinctBy { it.name } .distinctBy { it.name }
// Filter out those already downloaded. // Filter out those already downloaded.
.filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null } .filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
// Add chapters to queue from the start. // Add chapters to queue from the start.
.sortedByDescending { it.source_order } .sortedByDescending { it.source_order }
} }
// Runs in main thread (synchronization needed). // Runs in main thread (synchronization needed).
val chaptersToQueue = chaptersWithoutDir.await() val chaptersToQueue = chaptersWithoutDir.await()
// Filter out those already enqueued. // Filter out those already enqueued.
.filter { chapter -> queue.none { it.chapter.id == chapter.id } } .filter { chapter -> queue.none { it.chapter.id == chapter.id } }
// Create a download for each one. // Create a download for each one.
.map { Download(source, manga, it) } .map { Download(source, manga, it) }
if (chaptersToQueue.isNotEmpty()) { if (chaptersToQueue.isNotEmpty()) {
queue.addAll(chaptersToQueue) queue.addAll(chaptersToQueue)
// Initialize queue size.
notifier.initialQueueSize = queue.size
if (isRunning) { if (isRunning) {
// Send the list of downloads to the downloader. // Send the list of downloads to the downloader.
downloadsRelay.call(chaptersToQueue) downloadsRelay.call(chaptersToQueue)
} }
// Start downloader if needed // Start downloader if needed
if (autoStart) { if (autoStart && wasEmpty) {
DownloadService.start(this@Downloader.context) DownloadService.start(this@Downloader.context)
} }
} }
@ -247,59 +261,48 @@ class Downloader(
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer { private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
val chapterDirname = provider.getChapterDirName(download.chapter) val chapterDirname = provider.getChapterDirName(download.chapter)
val mangaDir = provider.getMangaDir(download.manga, download.source) 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) { val pageListObservable = if (download.pages == null) {
// Pull page list from network and add them to download object // Pull page list from network and add them to download object
download.source.fetchPageList(download.chapter) download.source.fetchPageList(download.chapter)
.doOnNext { pages -> .doOnNext { pages ->
if (pages.isEmpty()) { if (pages.isEmpty()) {
throw Exception("Page list is empty") throw Exception("Page list is empty")
}
download.pages = pages
} }
download.pages = pages
}
} else { } else {
// Or if the page list already exists, start from the file // Or if the page list already exists, start from the file
Observable.just(download.pages!!) Observable.just(download.pages!!)
} }
pageListObservable pageListObservable
.doOnNext { _ -> .doOnNext { _ ->
// Delete all temporary (unfinished) files // Delete all temporary (unfinished) files
tmpDir.listFiles() tmpDir.listFiles()
?.filter { it.name!!.endsWith(".tmp") } ?.filter { it.name!!.endsWith(".tmp") }
?.forEach { it.delete() } ?.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
}
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> { private fun getOrDownloadImage(page: Page, download: Download, tmpDir: UniFile): Observable<Page> {
// If the image URL is empty, do nothing // If the image URL is empty, do nothing
if (page.imageUrl == null) if (page.imageUrl == null) {
return Observable.just(page) return Observable.just(page)
}
val filename = String.format("%03d", page.number) val filename = String.format("%03d", page.number)
val tmpFile = tmpDir.findFile("$filename.tmp") val tmpFile = tmpDir.findFile("$filename.tmp")
@ -325,26 +329,27 @@ class Downloader(
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") } val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") }
// If the image is already downloaded, do nothing. Otherwise download from network // If the image is already downloaded, do nothing. Otherwise download from network
val pageObservable = if (imageFile != null) val pageObservable = when {
Observable.just(imageFile) imageFile != null -> Observable.just(imageFile)
else chapterCache.isImageInCache(page.imageUrl!!) -> copyImageFromCache(chapterCache.getImageFile(page.imageUrl!!), tmpDir, filename)
downloadImage(page, download.source, tmpDir, filename) else -> downloadImage(page, download.source, tmpDir, filename)
}
return pageObservable return pageObservable
// When the image is ready, set image path, progress (just in case) and status // When the image is ready, set image path, progress (just in case) and status
.doOnNext { file -> .doOnNext { file ->
page.uri = file.uri page.uri = file.uri
page.progress = 100 page.progress = 100
download.downloadedImages++ download.downloadedImages++
page.status = Page.READY page.status = Page.READY
} }
.map { page } .map { page }
// Mark this page as error and allow to download the remaining // Mark this page as error and allow to download the remaining
.onErrorReturn { .onErrorReturn {
page.progress = 0 page.progress = 0
page.status = Page.ERROR page.status = Page.ERROR
page page
} }
} }
/** /**
@ -359,30 +364,43 @@ class Downloader(
page.status = Page.DOWNLOAD_IMAGE page.status = Page.DOWNLOAD_IMAGE
page.progress = 0 page.progress = 0
return source.fetchImage(page) return source.fetchImage(page)
.map { response -> .map { response ->
val file = tmpDir.createFile("$filename.tmp") val file = tmpDir.createFile("$filename.tmp")
try { try {
response.body!!.source().saveTo(file.openOutputStream()) response.body!!.source().saveTo(file.openOutputStream())
val extension = getImageExtension(response, file) val extension = getImageExtension(response, file)
file.renameTo("$filename.$extension") file.renameTo("$filename.$extension")
} catch (e: Exception) { } catch (e: Exception) {
// [EXH] response.close()
XLog.w("> Failed to fetch image!", e) file.delete()
XLog.w("> (source.id: %s, source.name: %s, page.index: %s, page.url: %s, page.imageUrl: %s)", throw e
source.id,
source.name,
page.index,
page.url,
page.imageUrl)
response.close()
file.delete()
throw e
}
file
} }
// Retry 3 times, waiting 2, 4 and 8 seconds between attempts. file
.retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline())) }
// 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 tmpDir the directory where the download is currently stored.
* @param dirname the real (non temporary) directory name of the download. * @param dirname the real (non temporary) directory name of the download.
*/ */
private fun ensureSuccessfulDownload(download: Download, mangaDir: UniFile, private fun ensureSuccessfulDownload(
tmpDir: UniFile, dirname: String) { download: Download,
mangaDir: UniFile,
tmpDir: UniFile,
dirname: String
) {
// Ensure that the chapter folder has all the images. // Ensure that the chapter folder has all the images.
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") } val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
@ -442,9 +463,6 @@ class Downloader(
queue.remove(download) queue.remove(download)
} }
if (areAllDownloadsFinished()) { if (areAllDownloadsFinished()) {
if (notifier.isSingleChapter && !notifier.errorThrown) {
notifier.onDownloadCompleted(download, queue)
}
DownloadService.stop(context) DownloadService.stop(context)
} }
} }
@ -456,4 +474,7 @@ class Downloader(
return queue.none { it.status <= Download.DOWNLOADING } return queue.none { it.status <= Download.DOWNLOADING }
} }
companion object {
const val TMP_DIR_SUFFIX = "_tmp"
}
} }

View File

@ -10,28 +10,48 @@ class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
var pages: List<Page>? = null 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) { set(status) {
field = status field = status
statusSubject?.onNext(this) 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>?) { fun setStatusSubject(subject: PublishSubject<Download>?) {
statusSubject = subject statusSubject = subject
} }
companion object { fun setStatusCallback(f: ((Download) -> Unit)?) {
statusCallback = f
}
companion object {
const val NOT_DOWNLOADED = 0 const val NOT_DOWNLOADED = 0
const val QUEUE = 1 const val QUEUE = 1
const val DOWNLOADING = 2 const val DOWNLOADING = 2
const val DOWNLOADED = 3 const val DOWNLOADED = 3
const val ERROR = 4 const val ERROR = 4
} }
} }

View File

@ -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.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadStore import eu.kanade.tachiyomi.data.download.DownloadStore
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import java.util.concurrent.CopyOnWriteArrayList
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.util.concurrent.CopyOnWriteArrayList
class DownloadQueue( class DownloadQueue(
private val store: DownloadStore, private val store: DownloadStore,
private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>()) private val queue: MutableList<Download> = CopyOnWriteArrayList()
: List<Download> by queue { ) : List<Download> by queue {
private val statusSubject = PublishSubject.create<Download>() private val statusSubject = PublishSubject.create<Download>()
@ -21,6 +21,7 @@ class DownloadQueue(
fun addAll(downloads: List<Download>) { fun addAll(downloads: List<Download>) {
downloads.forEach { download -> downloads.forEach { download ->
download.setStatusSubject(statusSubject) download.setStatusSubject(statusSubject)
download.setStatusCallback(::setPagesFor)
download.status = Download.QUEUE download.status = Download.QUEUE
} }
queue.addAll(downloads) queue.addAll(downloads)
@ -32,6 +33,10 @@ class DownloadQueue(
val removed = queue.remove(download) val removed = queue.remove(download)
store.remove(download) store.remove(download)
download.setStatusSubject(null) download.setStatusSubject(null)
download.setStatusCallback(null)
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
download.status = Download.NOT_DOWNLOADED
}
if (removed) { if (removed) {
updatedRelay.call(Unit) updatedRelay.call(Unit)
} }
@ -42,7 +47,9 @@ class DownloadQueue(
} }
fun remove(chapters: List<Chapter>) { fun remove(chapters: List<Chapter>) {
for (chapter in chapters) { remove(chapter) } for (chapter in chapters) {
remove(chapter)
}
} }
fun remove(manga: Manga) { fun remove(manga: Manga) {
@ -52,6 +59,10 @@ class DownloadQueue(
fun clear() { fun clear() {
queue.forEach { download -> queue.forEach { download ->
download.setStatusSubject(null) download.setStatusSubject(null)
download.setStatusCallback(null)
if (download.status == Download.DOWNLOADING || download.status == Download.QUEUE) {
download.status = Download.NOT_DOWNLOADED
}
} }
queue.clear() queue.clear()
store.clear() store.clear()
@ -64,35 +75,35 @@ class DownloadQueue(
fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer() fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer() fun getUpdatedObservable(): Observable<List<Download>> = updatedRelay.onBackpressureBuffer()
.startWith(Unit) .startWith(Unit)
.map { this } .map { this }
fun getProgressObservable(): Observable<Download> { private fun setPagesFor(download: Download) {
return statusSubject.onBackpressureBuffer() if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
.startWith(getActiveDownloads()) setPagesSubject(download.pages, null)
.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)
}
} }
} }
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) }
}
} }

View File

@ -5,7 +5,12 @@ import android.util.Log
import com.bumptech.glide.Priority import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher 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> { 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) data = FileInputStream(file)
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
if (Log.isLoggable(TAG, Log.DEBUG)) { if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to open file", e) Timber.d(e, "Failed to open file")
} }
callback.onLoadFailed(e) callback.onLoadFailed(e)
return return
@ -48,4 +53,4 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
override fun getDataSource(): DataSource { override fun getDataSource(): DataSource {
return DataSource.LOCAL return DataSource.LOCAL
} }
} }

View File

@ -16,44 +16,48 @@ import java.io.InputStream
* @param manga the manga of the cover to load. * @param manga the manga of the cover to load.
* @param file the file where this cover should be. It may exists or not. * @param file the file where this cover should be. It may exists or not.
*/ */
class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>, class LibraryMangaUrlFetcher(
private val manga: Manga, private val networkFetcher: DataFetcher<InputStream>,
private val file: File) private val manga: Manga,
: FileFetcher(file) { private val file: File
) :
FileFetcher(file) {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) { override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
if (!file.exists()) { if (!file.exists()) {
networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> { networkFetcher.loadData(
override fun onDataReady(data: InputStream?) { priority,
if (data != null) { object : DataFetcher.DataCallback<InputStream> {
val tmpFile = File(file.path + ".tmp") override fun onDataReady(data: InputStream?) {
try { if (data != null) {
// Retrieve destination stream, create parent folders if needed. val tmpFile = File(file.path + ".tmp")
val output = try { try {
tmpFile.outputStream() // Retrieve destination stream, create parent folders if needed.
} catch (e: FileNotFoundException) { val output = try {
tmpFile.parentFile.mkdirs() tmpFile.outputStream()
tmpFile.outputStream() } catch (e: FileNotFoundException) {
} tmpFile.parentFile.mkdirs()
tmpFile.outputStream()
}
// Copy the file and rename to the original. // Copy the file and rename to the original.
data.use { output.use { data.copyTo(output) } } data.use { output.use { data.copyTo(output) } }
tmpFile.renameTo(file) tmpFile.renameTo(file)
loadFromFile(callback) loadFromFile(callback)
} catch (e: Exception) { } catch (e: Exception) {
tmpFile.delete() tmpFile.delete()
callback.onLoadFailed(e) 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 { } else {
loadFromFile(callback) loadFromFile(callback)
} }
@ -68,5 +72,4 @@ class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream
super.cancel() super.cancel()
networkFetcher.cancel() networkFetcher.cancel()
} }
}
}

View File

@ -24,4 +24,4 @@ class MangaSignature(manga: Manga, file: File) : Key {
override fun updateDiskCacheKey(md: MessageDigest) { override fun updateDiskCacheKey(md: MessageDigest) {
md.update(key.toByteArray(Key.CHARSET)) md.update(key.toByteArray(Key.CHARSET))
} }
} }

View File

@ -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)

View File

@ -3,18 +3,22 @@ package eu.kanade.tachiyomi.data.glide
import android.util.LruCache import android.util.LruCache
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
import com.bumptech.glide.load.Options 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.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import java.io.File
import java.io.InputStream
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy 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. * 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. * @param context the application context.
*/ */
class MangaModelLoader : ModelLoader<Manga, InputStream> { class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
/** /**
* Cover cache where persistent covers are stored. * Cover cache where persistent covers are stored.
@ -56,18 +60,18 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
private val cachedHeaders = hashMapOf<Long, LazyHeaders>() 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> { override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MangaThumbnail, InputStream> {
return MangaModelLoader() return MangaThumbnailModelLoader()
} }
override fun teardown() {} override fun teardown() {}
} }
override fun handles(model: Manga): Boolean { override fun handles(model: MangaThumbnail): Boolean {
return true 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 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. * @param height the height of the view where the resource will be loaded.
*/ */
override fun buildLoadData(manga: Manga, width: Int, height: Int, override fun buildLoadData(
options: Options): ModelLoader.LoadData<InputStream>? { mangaThumbnail: MangaThumbnail,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<InputStream>? {
// Check thumbnail is not null or empty // Check thumbnail is not null or empty
val url = manga.thumbnail_url val url = mangaThumbnail.url
if (url == null || url.isEmpty()) { if (url == null || url.isEmpty()) {
return null return null
} }
if (url.startsWith("http")) { val manga = mangaThumbnail.manga
if (url.startsWith("http", true)) {
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
val glideUrl = GlideUrl(url, getHeaders(manga, source)) val glideUrl = GlideUrl(url, getHeaders(manga, source))
@ -118,7 +128,7 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
* *
* @param manga the model. * @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 if (source == null) return LazyHeaders.DEFAULT
return cachedHeaders.getOrPut(manga.source) { return cachedHeaders.getOrPut(manga.source) {
@ -142,5 +152,4 @@ class MangaModelLoader : ModelLoader<Manga, InputStream> {
value value
} }
} }
} }

View File

@ -14,10 +14,10 @@ import java.io.InputStream
class PassthroughModelLoader : ModelLoader<InputStream, InputStream> { class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
override fun buildLoadData( override fun buildLoadData(
model: InputStream, model: InputStream,
width: Int, width: Int,
height: Int, height: Int,
options: Options options: Options
): ModelLoader.LoadData<InputStream>? { ): ModelLoader.LoadData<InputStream>? {
return ModelLoader.LoadData(ObjectKey(model), Fetcher(model)) return ModelLoader.LoadData(ObjectKey(model), Fetcher(model))
} }
@ -49,12 +49,11 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
} }
override fun loadData( override fun loadData(
priority: Priority, priority: Priority,
callback: DataFetcher.DataCallback<in InputStream> callback: DataFetcher.DataCallback<in InputStream>
) { ) {
callback.onDataReady(stream) callback.onDataReady(stream)
} }
} }
/** /**
@ -63,12 +62,11 @@ class PassthroughModelLoader : ModelLoader<InputStream, InputStream> {
class Factory : ModelLoaderFactory<InputStream, InputStream> { class Factory : ModelLoaderFactory<InputStream, InputStream> {
override fun build( override fun build(
multiFactory: MultiModelLoaderFactory multiFactory: MultiModelLoaderFactory
): ModelLoader<InputStream, InputStream> { ): ModelLoader<InputStream, InputStream> {
return PassthroughModelLoader() return PassthroughModelLoader()
} }
override fun teardown() {} override fun teardown() {}
} }
} }

View File

@ -13,11 +13,10 @@ import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.module.AppGlideModule import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import java.io.InputStream
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.io.InputStream
/** /**
* Class used to update Glide module settings * Class used to update Glide module settings
@ -28,16 +27,21 @@ class TachiGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) { override fun applyOptions(context: Context, builder: GlideBuilder) {
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024)) builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565)) builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
builder.setDefaultTransitionOptions(Drawable::class.java, builder.setDefaultTransitionOptions(
DrawableTransitionOptions.withCrossFade()) Drawable::class.java,
DrawableTransitionOptions.withCrossFade()
)
} }
override fun registerComponents(context: Context, glide: Glide, registry: Registry) { override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client) val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory) registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory()) registry.append(MangaThumbnail::class.java, InputStream::class.java, MangaThumbnailModelLoader.Factory())
registry.append(InputStream::class.java, InputStream::class.java, PassthroughModelLoader registry.append(
.Factory()) InputStream::class.java, InputStream::class.java,
PassthroughModelLoader
.Factory()
)
} }
} }

View File

@ -1,47 +1,58 @@
package eu.kanade.tachiyomi.data.library package eu.kanade.tachiyomi.data.library
import com.evernote.android.job.Job import android.content.Context
import com.evernote.android.job.JobManager import androidx.work.Constraints
import com.evernote.android.job.JobRequest 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.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault import java.util.concurrent.TimeUnit
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get 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) LibraryUpdateService.start(context)
return Job.Result.SUCCESS return Result.success()
} }
companion object { 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 preferences = Injekt.get<PreferencesHelper>()
val interval = prefInterval ?: preferences.libraryUpdateInterval().getOrDefault() val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
if (interval > 0) { if (interval > 0) {
val restrictions = preferences.libraryUpdateRestriction() val restrictions = preferences.libraryUpdateRestriction()!!
val acRestriction = "ac" in restrictions val acRestriction = "ac" in restrictions
val wifiRestriction = if ("wifi" in restrictions) val wifiRestriction = if ("wifi" in restrictions) {
JobRequest.NetworkType.UNMETERED NetworkType.UNMETERED
else } else {
JobRequest.NetworkType.CONNECTED NetworkType.CONNECTED
}
JobRequest.Builder(TAG) val constraints = Constraints.Builder()
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000) .setRequiredNetworkType(wifiRestriction)
.setRequiredNetworkType(wifiRestriction) .setRequiresCharging(acRestriction)
.setRequiresCharging(acRestriction) .build()
.setRequirementsEnforced(true)
.setUpdateCurrent(true) val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
.build() interval.toLong(), TimeUnit.HOURS,
.schedule() 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)
}
} }
} }

View File

@ -8,8 +8,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga
object LibraryUpdateRanker { object LibraryUpdateRanker {
val rankingScheme = listOf( val rankingScheme = listOf(
(this::lexicographicRanking)(), (this::lexicographicRanking)(),
(this::latestFirstRanking)()) (this::latestFirstRanking)()
)
/** /**
* Provides a total ordering over all the Mangas. * Provides a total ordering over all the Mangas.
@ -22,7 +23,7 @@ object LibraryUpdateRanker {
*/ */
fun latestFirstRanking(): Comparator<Manga> { fun latestFirstRanking(): Comparator<Manga> {
return Comparator { mangaFirst: Manga, return Comparator { mangaFirst: Manga,
mangaSecond: Manga -> mangaSecond: Manga ->
compareValues(mangaSecond.last_update, mangaFirst.last_update) compareValues(mangaSecond.last_update, mangaFirst.last_update)
} }
} }
@ -35,9 +36,8 @@ object LibraryUpdateRanker {
*/ */
fun lexicographicRanking(): Comparator<Manga> { fun lexicographicRanking(): Comparator<Manga> {
return Comparator { mangaFirst: Manga, return Comparator { mangaFirst: Manga,
mangaSecond: Manga -> mangaSecond: Manga ->
compareValues(mangaFirst.title, mangaSecond.title) compareValues(mangaFirst.title, mangaSecond.title)
} }
} }
}
}

View File

@ -1,12 +1,19 @@
package eu.kanade.tachiyomi.data.library package eu.kanade.tachiyomi.data.library
import android.app.Notification
import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import androidx.core.app.NotificationCompat 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.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category 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.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService 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.LibraryUpdateRanker.rankingScheme
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
import eu.kanade.tachiyomi.data.notification.NotificationReceiver import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.isServiceRunning import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.notificationManager import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.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 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.Observable
import rx.Subscription import rx.Subscription
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get 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 * 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. * destroyed.
*/ */
class LibraryUpdateService( class LibraryUpdateService(
val db: DatabaseHelper = Injekt.get(), val db: DatabaseHelper = Injekt.get(),
val sourceManager: SourceManager = Injekt.get(), val sourceManager: SourceManager = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(),
val downloadManager: DownloadManager = Injekt.get(), val downloadManager: DownloadManager = Injekt.get(),
val trackManager: TrackManager = Injekt.get() val trackManager: TrackManager = Injekt.get()
) : Service() { ) : Service() {
/** /**
@ -76,13 +89,15 @@ class LibraryUpdateService(
/** /**
* Cached progress notification to avoid creating a lot. * Cached progress notification to avoid creating a lot.
*/ */
private val progressNotification by lazy { NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY) private val progressNotificationBuilder by lazy {
.setContentTitle(getString(R.string.app_name)) notificationBuilder(Notifications.CHANNEL_LIBRARY) {
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img) setContentTitle(getString(R.string.app_name))
.setLargeIcon(updateNotifier.notificationBitmap) setSmallIcon(R.drawable.ic_refresh_24dp)
.setOngoing(true) setLargeIcon(notificationBitmap)
.setOnlyAlertOnce(true) setOngoing(true)
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent) setOnlyAlertOnce(true)
addAction(R.drawable.ic_close_24dp, getString(android.R.string.cancel), cancelIntent)
}
} }
/** /**
@ -90,8 +105,8 @@ class LibraryUpdateService(
*/ */
enum class Target { enum class Target {
CHAPTERS, // Manga chapters CHAPTERS, // Manga chapters
DETAILS, // Manga metadata DETAILS, // Manga metadata
TRACKING // Tracking metadata TRACKING // Tracking metadata
} }
companion object { companion object {
@ -106,6 +121,10 @@ class LibraryUpdateService(
*/ */
const val KEY_TARGET = "target" 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. * Returns the status of the service.
* *
@ -123,8 +142,9 @@ class LibraryUpdateService(
* @param context the application context. * @param context the application context.
* @param category a specific category to update, or null for global update. * @param category a specific category to update, or null for global update.
* @param target defines what should be updated. * @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)) { if (!isRunning(context)) {
val intent = Intent(context, LibraryUpdateService::class.java).apply { val intent = Intent(context, LibraryUpdateService::class.java).apply {
putExtra(KEY_TARGET, target) putExtra(KEY_TARGET, target)
@ -135,7 +155,11 @@ class LibraryUpdateService(
} else { } else {
context.startForegroundService(intent) context.startForegroundService(intent)
} }
return true
} }
return false
} }
/** /**
@ -146,7 +170,6 @@ class LibraryUpdateService(
fun stop(context: Context) { fun stop(context: Context) {
context.stopService(Intent(context, LibraryUpdateService::class.java)) context.stopService(Intent(context, LibraryUpdateService::class.java))
} }
} }
/** /**
@ -155,9 +178,10 @@ class LibraryUpdateService(
*/ */
override fun onCreate() { override fun onCreate() {
super.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( wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock") PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock"
)
wakeLock.acquire() wakeLock.acquire()
} }
@ -189,37 +213,41 @@ class LibraryUpdateService(
* @return the start value of the command. * @return the start value of the command.
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 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 val target = intent.getSerializableExtra(KEY_TARGET) as? Target
?: return Service.START_NOT_STICKY ?: return START_NOT_STICKY
// Unsubscribe from any previous subscription if needed. // Unsubscribe from any previous subscription if needed.
subscription?.unsubscribe() subscription?.unsubscribe()
// Update favorite manga. Destroy service when completed or in case of an error. // Update favorite manga. Destroy service when completed or in case of an error.
subscription = Observable subscription = Observable
.defer { .defer {
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault() val selectedScheme = preferences.libraryUpdatePrioritization().get()
val mangaList = getMangaToUpdate(intent, target) val mangaList = getMangaToUpdate(intent, target)
.sortedWith(rankingScheme[selectedScheme]) .sortedWith(rankingScheme[selectedScheme])
// Update either chapter list or manga details. // Update either chapter list or manga details.
when (target) { when (target) {
Target.CHAPTERS -> updateChapterList(mangaList) Target.CHAPTERS -> updateChapterList(mangaList)
Target.DETAILS -> updateDetails(mangaList) Target.DETAILS -> updateDetails(mangaList)
Target.TRACKING -> updateTrackings(mangaList) Target.TRACKING -> updateTrackings(mangaList)
}
} }
.subscribeOn(Schedulers.io()) }
.subscribe({ .subscribeOn(Schedulers.io())
}, { .subscribe(
{
},
{
Timber.e(it) Timber.e(it)
stopSelf(startId) stopSelf(startId)
}, { },
{
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> { fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1) val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
var listToUpdate = if (categoryId != -1) var listToUpdate = if (categoryId != -1) {
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId } db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
else { } else {
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt) val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
if (categoriesToUpdate.isNotEmpty()) if (categoriesToUpdate.isNotEmpty()) {
db.getLibraryMangas().executeAsBlocking() db.getLibraryMangas().executeAsBlocking()
.filter { it.category in categoriesToUpdate } .filter { it.category in categoriesToUpdate }
.distinctBy { it.id } .distinctBy { it.id }
else } else {
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id } db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
}
} }
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) { if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED } listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
} }
@ -264,66 +292,73 @@ class LibraryUpdateService(
// Initialize the variables holding the progress of the updates. // Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0) val count = AtomicInteger(0)
// List containing new updates // List containing new updates
val newUpdates = ArrayList<Manga>() val newUpdates = ArrayList<Pair<LibraryManga, Array<Chapter>>>()
// list containing failed updates // List containing failed updates
val failedUpdates = ArrayList<Manga>() val failedUpdates = ArrayList<Manga>()
// List containing categories that get included in downloads. // 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. // 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 // Boolean to determine if DownloadManager has downloads
var hasDownloads = false var hasDownloads = false
// Emit each manga and update it sequentially. // Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate) return Observable.from(mangaToUpdate)
// Notify manga that will update. // Notify manga that will update.
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
// Update the chapters of the manga. // Update the chapters of the manga.
.concatMap { manga -> .concatMap { manga ->
if(manga.source in LIBRARY_UPDATE_EXCLUDED_SOURCES) { if(manga.source in LIBRARY_UPDATE_EXCLUDED_SOURCES) {
// Ignore EXH manga, updating chapters for every manga will get you banned // Ignore EXH manga, updating chapters for every manga will get you banned
Observable.empty() Observable.empty()
} else { } else {
updateManga(manga) updateManga(manga)
// If there's any error, return empty update and continue. // If there's any error, return empty update and continue.
.onErrorReturn { .onErrorReturn {
failedUpdates.add(manga) failedUpdates.add(manga)
Pair(emptyList(), emptyList()) 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() } // Convert to the manga that contains new chapters.
.doOnNext { .map {
if (downloadNew && (categoriesToDownload.isEmpty() || Pair(
manga.category in categoriesToDownload)) { manga,
(it.first.sortedByDescending { ch -> ch.source_order }.toTypedArray())
downloadChapters(manga, it.first) )
hasDownloads = true }
} }
} // Add manga with new chapters to the list.
// Convert to the manga that contains new chapters. .doOnNext { manga ->
.map { 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()) { if (failedUpdates.isNotEmpty()) {
Timber.e("Failed updating: ${failedUpdates.map { it.title }}") Timber.e("Failed updating: ${failedUpdates.map { it.title }}")
}
cancelProgressNotification()
} }
cancelProgressNotification()
}
.map { manga -> manga.first }
} }
fun downloadChapters(manga: Manga, chapters: List<Chapter>) { fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
@ -346,7 +381,7 @@ class LibraryUpdateService(
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> { fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty() val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty()
return source.fetchChapterList(manga) 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. // Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate) return Observable.from(mangaToUpdate)
// Notify manga that will update. // Notify manga that will update.
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } .doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
// Update the details of the manga. // Update the details of the manga.
.concatMap { manga -> .concatMap { manga ->
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
?: return@concatMap Observable.empty<LibraryManga>() ?: return@concatMap Observable.empty<LibraryManga>()
source.fetchMangaDetails(manga) source.fetchMangaDetails(manga)
.map { networkManga -> .map { networkManga ->
manga.copyFrom(networkManga) manga.copyFrom(networkManga)
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
manga manga
} }
.onErrorReturn { manga } .onErrorReturn { manga }
} }
.doOnCompleted { .doOnCompleted {
cancelProgressNotification() cancelProgressNotification()
} }
} }
/** /**
@ -394,28 +429,28 @@ class LibraryUpdateService(
// Emit each manga and update it sequentially. // Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate) return Observable.from(mangaToUpdate)
// Notify manga that will update. // Notify manga that will update.
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) } .doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
// Update the tracking details. // Update the tracking details.
.concatMap { manga -> .concatMap { manga ->
val tracks = db.getTracks(manga).executeAsBlocking() val tracks = db.getTracks(manga).executeAsBlocking()
Observable.from(tracks) Observable.from(tracks)
.concatMap { track -> .concatMap { track ->
val service = trackManager.getService(track.sync_id) val service = trackManager.getService(track.sync_id)
if (service != null && service in loggedServices) { if (service != null && service in loggedServices) {
service.refresh(track) service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() } .doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn { track } .onErrorReturn { track }
} else { } else {
Observable.empty() Observable.empty()
} }
} }
.map { manga } .map { manga }
} }
.doOnCompleted { .doOnCompleted {
cancelProgressNotification() cancelProgressNotification()
} }
} }
/** /**
@ -426,10 +461,116 @@ class LibraryUpdateService(
* @param total the total progress. * @param total the total progress.
*/ */
private fun showProgressNotification(manga: Manga, current: Int, total: Int) { private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification val title = if (preferences.hideNotificationContent()) {
.setContentTitle(manga.title) getString(R.string.notification_check_updates)
} else {
manga.title
}
notificationManager.notify(
Notifications.ID_LIBRARY_PROGRESS,
progressNotificationBuilder
.setContentTitle(title)
.setProgress(total, current, false) .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() { private fun cancelProgressNotification() {
notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS) 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)
}
} }

View File

@ -4,8 +4,9 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import java.io.File import java.io.File
/** /**
@ -48,7 +49,7 @@ object NotificationHandler {
*/ */
fun installApkPendingActivity(context: Context, uri: Uri): PendingIntent { fun installApkPendingActivity(context: Context, uri: Uri): PendingIntent {
val intent = Intent(Intent.ACTION_VIEW).apply { 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 flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
} }
return PendingIntent.getActivity(context, 0, intent, 0) return PendingIntent.getActivity(context, 0, intent, 0)

Some files were not shown because too many files have changed in this diff Show More