mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-22 03:54:18 -04:00
Merge pull request #1430 from kiwix/feature/macgills/#1386-use-fetch-downloads
Feature/macgills/#1386 use fetch downloads
This commit is contained in:
commit
f0b5344e4d
76
.idea/codeStyles/Project.xml
generated
76
.idea/codeStyles/Project.xml
generated
@ -10,14 +10,6 @@
|
||||
</value>
|
||||
</option>
|
||||
<option name="LINE_SEPARATOR" value=" " />
|
||||
<AndroidXmlCodeStyleSettings>
|
||||
<option name="LAYOUT_SETTINGS">
|
||||
<value>
|
||||
<option name="INSERT_BLANK_LINE_BEFORE_TAG" value="false" />
|
||||
<option name="INSERT_LINE_BREAK_AFTER_LAST_ATTRIBUTE" value="true" />
|
||||
</value>
|
||||
</option>
|
||||
</AndroidXmlCodeStyleSettings>
|
||||
<GroovyCodeStyleSettings>
|
||||
<option name="ALIGN_MULTILINE_LIST_OR_MAP" value="false" />
|
||||
<option name="ALIGN_NAMED_ARGS_IN_MAP" value="false" />
|
||||
@ -207,7 +199,7 @@
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||
<option name="TAB_SIZE" value="2" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
@ -215,28 +207,22 @@
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<NAME>class</NAME>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<NAME>layout</NAME>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<NAME>xmlns:android</NAME>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
@ -246,6 +232,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@ -256,6 +243,7 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
@ -265,8 +253,9 @@
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:layout_width</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
@ -275,8 +264,9 @@
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:layout_height</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
@ -285,8 +275,9 @@
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:layout_.*</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
@ -297,38 +288,25 @@
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>app:layout_.*</NAME>
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<NAME>.*(?<!style)$</NAME>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<NAME>style</NAME>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
|
@ -132,6 +132,9 @@ dependencies {
|
||||
implementation "io.objectbox:objectbox-rxjava:$objectboxVersion"
|
||||
implementation 'com.google.android.gms:play-services-location:17.0.0'
|
||||
|
||||
implementation "androidx.tonyodev.fetch2:xfetch2:$fetchVersion"
|
||||
implementation "androidx.tonyodev.fetch2okhttp:xfetch2okhttp:$fetchVersion"
|
||||
|
||||
testImplementation "org.junit.jupiter:junit-jupiter:5.4.2"
|
||||
testImplementation "io.mockk:mockk:1.9"
|
||||
testImplementation "org.assertj:assertj-core:3.11.1"
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -3,74 +3,6 @@
|
||||
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
|
||||
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
|
||||
"entities": [
|
||||
{
|
||||
"id": "1:7257718270326155947",
|
||||
"lastPropertyId": "17:8085320504542486236",
|
||||
"name": "DownloadEntity",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:2266566996008201697",
|
||||
"name": "id"
|
||||
},
|
||||
{
|
||||
"id": "2:1953917250527765737",
|
||||
"name": "downloadId"
|
||||
},
|
||||
{
|
||||
"id": "5:6575412958851693470",
|
||||
"name": "bookId"
|
||||
},
|
||||
{
|
||||
"id": "6:1075612111256674117",
|
||||
"name": "title"
|
||||
},
|
||||
{
|
||||
"id": "7:2831524841121029990",
|
||||
"name": "description"
|
||||
},
|
||||
{
|
||||
"id": "8:2334902404590133038",
|
||||
"name": "language"
|
||||
},
|
||||
{
|
||||
"id": "9:5087250349738158996",
|
||||
"name": "creator"
|
||||
},
|
||||
{
|
||||
"id": "10:6128960350043895299",
|
||||
"name": "publisher"
|
||||
},
|
||||
{
|
||||
"id": "11:3850323036475883785",
|
||||
"name": "date"
|
||||
},
|
||||
{
|
||||
"id": "12:5288623325038033644",
|
||||
"name": "url"
|
||||
},
|
||||
{
|
||||
"id": "13:2501711400901908648",
|
||||
"name": "articleCount"
|
||||
},
|
||||
{
|
||||
"id": "14:3550975911715416030",
|
||||
"name": "mediaCount"
|
||||
},
|
||||
{
|
||||
"id": "15:8949996430663588693",
|
||||
"name": "size"
|
||||
},
|
||||
{
|
||||
"id": "16:7554483297276446029",
|
||||
"name": "name"
|
||||
},
|
||||
{
|
||||
"id": "17:8085320504542486236",
|
||||
"name": "favIcon"
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
{
|
||||
"id": "3:5536749840871435068",
|
||||
"lastPropertyId": "16:6142333908132117423",
|
||||
@ -262,16 +194,113 @@
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
{
|
||||
"id": "8:8093454424037540087",
|
||||
"lastPropertyId": "23:5485468735259326535",
|
||||
"name": "FetchDownloadEntity",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:7366957113003324901",
|
||||
"name": "id"
|
||||
},
|
||||
{
|
||||
"id": "3:3174500111130052488",
|
||||
"name": "bookId"
|
||||
},
|
||||
{
|
||||
"id": "4:3949362784963767166",
|
||||
"name": "title"
|
||||
},
|
||||
{
|
||||
"id": "5:812546090900770347",
|
||||
"name": "description"
|
||||
},
|
||||
{
|
||||
"id": "6:3129463483413863468",
|
||||
"name": "language"
|
||||
},
|
||||
{
|
||||
"id": "7:3402286918039853548",
|
||||
"name": "creator"
|
||||
},
|
||||
{
|
||||
"id": "8:4732753967507809221",
|
||||
"name": "publisher"
|
||||
},
|
||||
{
|
||||
"id": "9:3239042532048399134",
|
||||
"name": "date"
|
||||
},
|
||||
{
|
||||
"id": "10:1136584919149973914",
|
||||
"name": "url"
|
||||
},
|
||||
{
|
||||
"id": "11:4252749008345744598",
|
||||
"name": "articleCount"
|
||||
},
|
||||
{
|
||||
"id": "12:8625493380854102341",
|
||||
"name": "mediaCount"
|
||||
},
|
||||
{
|
||||
"id": "13:2787210837560254021",
|
||||
"name": "size"
|
||||
},
|
||||
{
|
||||
"id": "14:2052022387195277817",
|
||||
"name": "name"
|
||||
},
|
||||
{
|
||||
"id": "15:1976493094677983679",
|
||||
"name": "favIcon"
|
||||
},
|
||||
{
|
||||
"id": "16:217454020763036675",
|
||||
"name": "etaInMilliSeconds"
|
||||
},
|
||||
{
|
||||
"id": "17:1136630637198901642",
|
||||
"name": "bytesDownloaded"
|
||||
},
|
||||
{
|
||||
"id": "18:8939019296899137627",
|
||||
"name": "totalSizeOfDownload"
|
||||
},
|
||||
{
|
||||
"id": "19:3378789699620971394",
|
||||
"name": "status"
|
||||
},
|
||||
{
|
||||
"id": "20:6867355950440828062",
|
||||
"name": "error"
|
||||
},
|
||||
{
|
||||
"id": "21:5555873126720275555",
|
||||
"name": "file"
|
||||
},
|
||||
{
|
||||
"id": "22:2724607601244650879",
|
||||
"name": "downloadId"
|
||||
},
|
||||
{
|
||||
"id": "23:5485468735259326535",
|
||||
"name": "progress"
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
}
|
||||
],
|
||||
"lastEntityId": "7:7635075139296819361",
|
||||
"lastEntityId": "8:8093454424037540087",
|
||||
"lastIndexId": "4:4868787482832538530",
|
||||
"lastRelationId": "0:0",
|
||||
"lastSequenceId": "0:0",
|
||||
"modelVersion": 4,
|
||||
"modelVersionParserMinimum": 4,
|
||||
"retiredEntityUids": [
|
||||
349148274283701276
|
||||
349148274283701276,
|
||||
7257718270326155947
|
||||
],
|
||||
"retiredIndexUids": [
|
||||
1293695782925933448,
|
||||
@ -297,7 +326,23 @@
|
||||
5620508895870653354,
|
||||
7273406943564025911,
|
||||
428251106490095982,
|
||||
5162677841083528491
|
||||
5162677841083528491,
|
||||
7886541039889727771,
|
||||
2266566996008201697,
|
||||
1953917250527765737,
|
||||
6575412958851693470,
|
||||
1075612111256674117,
|
||||
2831524841121029990,
|
||||
2334902404590133038,
|
||||
5087250349738158996,
|
||||
6128960350043895299,
|
||||
3850323036475883785,
|
||||
5288623325038033644,
|
||||
2501711400901908648,
|
||||
3550975911715416030,
|
||||
8949996430663588693,
|
||||
7554483297276446029,
|
||||
8085320504542486236
|
||||
],
|
||||
"retiredRelationUids": [],
|
||||
"version": 1
|
||||
|
@ -3,74 +3,6 @@
|
||||
"_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
|
||||
"_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
|
||||
"entities": [
|
||||
{
|
||||
"id": "1:7257718270326155947",
|
||||
"lastPropertyId": "17:8085320504542486236",
|
||||
"name": "DownloadEntity",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:2266566996008201697",
|
||||
"name": "id"
|
||||
},
|
||||
{
|
||||
"id": "2:1953917250527765737",
|
||||
"name": "downloadId"
|
||||
},
|
||||
{
|
||||
"id": "5:6575412958851693470",
|
||||
"name": "bookId"
|
||||
},
|
||||
{
|
||||
"id": "6:1075612111256674117",
|
||||
"name": "title"
|
||||
},
|
||||
{
|
||||
"id": "7:2831524841121029990",
|
||||
"name": "description"
|
||||
},
|
||||
{
|
||||
"id": "8:2334902404590133038",
|
||||
"name": "language"
|
||||
},
|
||||
{
|
||||
"id": "9:5087250349738158996",
|
||||
"name": "creator"
|
||||
},
|
||||
{
|
||||
"id": "10:6128960350043895299",
|
||||
"name": "publisher"
|
||||
},
|
||||
{
|
||||
"id": "11:3850323036475883785",
|
||||
"name": "date"
|
||||
},
|
||||
{
|
||||
"id": "12:5288623325038033644",
|
||||
"name": "url"
|
||||
},
|
||||
{
|
||||
"id": "13:2501711400901908648",
|
||||
"name": "articleCount"
|
||||
},
|
||||
{
|
||||
"id": "14:3550975911715416030",
|
||||
"name": "mediaCount"
|
||||
},
|
||||
{
|
||||
"id": "15:8949996430663588693",
|
||||
"name": "size"
|
||||
},
|
||||
{
|
||||
"id": "16:7554483297276446029",
|
||||
"name": "name"
|
||||
},
|
||||
{
|
||||
"id": "17:8085320504542486236",
|
||||
"name": "favIcon"
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
{
|
||||
"id": "3:5536749840871435068",
|
||||
"lastPropertyId": "16:6142333908132117423",
|
||||
@ -262,16 +194,113 @@
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
},
|
||||
{
|
||||
"id": "8:8093454424037540087",
|
||||
"lastPropertyId": "23:5485468735259326535",
|
||||
"name": "FetchDownloadEntity",
|
||||
"properties": [
|
||||
{
|
||||
"id": "1:7366957113003324901",
|
||||
"name": "id"
|
||||
},
|
||||
{
|
||||
"id": "3:3174500111130052488",
|
||||
"name": "bookId"
|
||||
},
|
||||
{
|
||||
"id": "4:3949362784963767166",
|
||||
"name": "title"
|
||||
},
|
||||
{
|
||||
"id": "5:812546090900770347",
|
||||
"name": "description"
|
||||
},
|
||||
{
|
||||
"id": "6:3129463483413863468",
|
||||
"name": "language"
|
||||
},
|
||||
{
|
||||
"id": "7:3402286918039853548",
|
||||
"name": "creator"
|
||||
},
|
||||
{
|
||||
"id": "8:4732753967507809221",
|
||||
"name": "publisher"
|
||||
},
|
||||
{
|
||||
"id": "9:3239042532048399134",
|
||||
"name": "date"
|
||||
},
|
||||
{
|
||||
"id": "10:1136584919149973914",
|
||||
"name": "url"
|
||||
},
|
||||
{
|
||||
"id": "11:4252749008345744598",
|
||||
"name": "articleCount"
|
||||
},
|
||||
{
|
||||
"id": "12:8625493380854102341",
|
||||
"name": "mediaCount"
|
||||
},
|
||||
{
|
||||
"id": "13:2787210837560254021",
|
||||
"name": "size"
|
||||
},
|
||||
{
|
||||
"id": "14:2052022387195277817",
|
||||
"name": "name"
|
||||
},
|
||||
{
|
||||
"id": "15:1976493094677983679",
|
||||
"name": "favIcon"
|
||||
},
|
||||
{
|
||||
"id": "16:217454020763036675",
|
||||
"name": "etaInMilliSeconds"
|
||||
},
|
||||
{
|
||||
"id": "17:1136630637198901642",
|
||||
"name": "bytesDownloaded"
|
||||
},
|
||||
{
|
||||
"id": "18:8939019296899137627",
|
||||
"name": "totalSizeOfDownload"
|
||||
},
|
||||
{
|
||||
"id": "19:3378789699620971394",
|
||||
"name": "status"
|
||||
},
|
||||
{
|
||||
"id": "20:6867355950440828062",
|
||||
"name": "error"
|
||||
},
|
||||
{
|
||||
"id": "21:5555873126720275555",
|
||||
"name": "file"
|
||||
},
|
||||
{
|
||||
"id": "22:2724607601244650879",
|
||||
"name": "downloadId"
|
||||
},
|
||||
{
|
||||
"id": "23:5485468735259326535",
|
||||
"name": "progress"
|
||||
}
|
||||
],
|
||||
"relations": []
|
||||
}
|
||||
],
|
||||
"lastEntityId": "7:7635075139296819361",
|
||||
"lastEntityId": "8:8093454424037540087",
|
||||
"lastIndexId": "4:4868787482832538530",
|
||||
"lastRelationId": "0:0",
|
||||
"lastSequenceId": "0:0",
|
||||
"modelVersion": 4,
|
||||
"modelVersionParserMinimum": 4,
|
||||
"retiredEntityUids": [
|
||||
349148274283701276
|
||||
349148274283701276,
|
||||
7257718270326155947
|
||||
],
|
||||
"retiredIndexUids": [
|
||||
1293695782925933448,
|
||||
@ -297,7 +326,23 @@
|
||||
5620508895870653354,
|
||||
7273406943564025911,
|
||||
428251106490095982,
|
||||
5162677841083528491
|
||||
5162677841083528491,
|
||||
7886541039889727771,
|
||||
2266566996008201697,
|
||||
1953917250527765737,
|
||||
6575412958851693470,
|
||||
1075612111256674117,
|
||||
2831524841121029990,
|
||||
2334902404590133038,
|
||||
5087250349738158996,
|
||||
6128960350043895299,
|
||||
3850323036475883785,
|
||||
5288623325038033644,
|
||||
2501711400901908648,
|
||||
3550975911715416030,
|
||||
8949996430663588693,
|
||||
7554483297276446029,
|
||||
8085320504542486236
|
||||
],
|
||||
"retiredRelationUids": [],
|
||||
"version": 1
|
||||
|
@ -27,7 +27,7 @@ import java.util.Stack
|
||||
*/
|
||||
class KiwixMockServer {
|
||||
|
||||
val queuedResponses: Stack<MockResponse> = Stack()
|
||||
var forcedResponse: MockResponse? = null
|
||||
|
||||
private val mockWebServer = MockWebServer().apply {
|
||||
start(TEST_PORT)
|
||||
@ -44,8 +44,8 @@ class KiwixMockServer {
|
||||
mockWebServer.setDispatcher(object : Dispatcher() {
|
||||
override fun dispatch(request: RecordedRequest) =
|
||||
mapOfPathsToResponses[request.path]?.let(::successfulResponse)
|
||||
?: queuedResponses.popOrNull()?.let { return@let it }
|
||||
?: throw RuntimeException("No response mapped for ${request.path}")
|
||||
?: forcedResponse?.let { return@let it }
|
||||
?: throw RuntimeException("No response mapped for ${request.path}\nmapped $mapOfPathsToResponses\nqueued $forcedResponse")
|
||||
})
|
||||
}
|
||||
|
||||
@ -54,8 +54,8 @@ class KiwixMockServer {
|
||||
setBody(bodyObject.asXmlString())
|
||||
}
|
||||
|
||||
fun queueResponse(mockResponse: MockResponse) {
|
||||
queuedResponses.push(mockResponse)
|
||||
fun forceResponse(mockResponse: MockResponse) {
|
||||
forcedResponse = mockResponse
|
||||
}
|
||||
|
||||
private fun <E> Stack<E>.popOrNull() =
|
||||
|
@ -21,10 +21,11 @@ package org.kiwix.kiwixmobile.main;
|
||||
import android.Manifest;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import com.schibsted.spain.barista.interaction.BaristaMenuClickInteractions;
|
||||
import com.schibsted.spain.barista.interaction.BaristaSleepInteractions;
|
||||
import com.schibsted.spain.barista.rule.BaristaRule;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@ -38,9 +39,9 @@ import static org.kiwix.kiwixmobile.utils.StandardActions.enterSettings;
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class MainActivityTest {
|
||||
|
||||
@Rule
|
||||
public BaristaRule<MainActivity> activityTestRule = BaristaRule.create(MainActivity.class);
|
||||
public ActivityTestRule<MainActivity> activityTestRule =
|
||||
new ActivityTestRule<>(MainActivity.class);
|
||||
@Rule
|
||||
public GrantPermissionRule readPermissionRule =
|
||||
GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
@ -48,35 +49,28 @@ public class MainActivityTest {
|
||||
public GrantPermissionRule writePermissionRule =
|
||||
GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
|
||||
@Test
|
||||
public void MainActivitySimple() {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void navigateHelp() {
|
||||
activityTestRule.launchActivity();
|
||||
BaristaSleepInteractions.sleep(TEST_PAUSE_MS);
|
||||
BaristaMenuClickInteractions.clickMenu(getResourceString(R.string.menu_help));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Ignore("This is hanging on travis")
|
||||
//TODO fix as part of https://github.com/kiwix/kiwix-android/issues/1428
|
||||
public void navigateSettings() {
|
||||
activityTestRule.launchActivity();
|
||||
BaristaSleepInteractions.sleep(TEST_PAUSE_MS);
|
||||
enterSettings();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void navigateBookmarks() {
|
||||
activityTestRule.launchActivity();
|
||||
BaristaSleepInteractions.sleep(TEST_PAUSE_MS);
|
||||
BaristaMenuClickInteractions.clickMenu(getResourceString(R.string.menu_bookmarks));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void navigateDeviceContent() {
|
||||
activityTestRule.launchActivity();
|
||||
BaristaSleepInteractions.sleep(TEST_PAUSE_MS);
|
||||
BaristaMenuClickInteractions.clickMenu(getResourceString(R.string.menu_zim_manager));
|
||||
BaristaSleepInteractions.sleep(TEST_PAUSE_MS);
|
||||
@ -85,7 +79,6 @@ public class MainActivityTest {
|
||||
|
||||
@Test
|
||||
public void navigateOnlineContent() {
|
||||
activityTestRule.launchActivity();
|
||||
BaristaSleepInteractions.sleep(TEST_PAUSE_MS);
|
||||
BaristaMenuClickInteractions.clickMenu(getResourceString(R.string.menu_zim_manager));
|
||||
BaristaSleepInteractions.sleep(TEST_PAUSE_MS);
|
||||
@ -94,7 +87,6 @@ public class MainActivityTest {
|
||||
|
||||
@Test
|
||||
public void navigateDownloadingContent() {
|
||||
activityTestRule.launchActivity();
|
||||
BaristaSleepInteractions.sleep(TEST_PAUSE_MS);
|
||||
BaristaMenuClickInteractions.clickMenu(getResourceString(R.string.menu_zim_manager));
|
||||
BaristaSleepInteractions.sleep(TEST_PAUSE_MS);
|
||||
|
@ -20,14 +20,15 @@ package org.kiwix.kiwixmobile.splash;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.test.espresso.intent.Intents;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.ActivityTestRule;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import com.schibsted.spain.barista.interaction.BaristaSleepInteractions;
|
||||
import com.schibsted.spain.barista.rule.BaristaRule;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@ -47,8 +48,8 @@ import static org.kiwix.kiwixmobile.utils.SharedPreferenceUtil.PREF_SHOW_INTRO;
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class SplashActivityTest {
|
||||
|
||||
@Rule
|
||||
public BaristaRule<SplashActivity> activityTestRule = BaristaRule.create(SplashActivity.class);
|
||||
private ActivityTestRule<SplashActivity> activityTestRule =
|
||||
new ActivityTestRule<>(SplashActivity.class, true, false);
|
||||
@Rule
|
||||
public GrantPermissionRule readPermissionRule =
|
||||
GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
@ -65,7 +66,8 @@ public class SplashActivityTest {
|
||||
|
||||
@Test
|
||||
public void testFirstRun() {
|
||||
activityTestRule.launchActivity();
|
||||
shouldShowIntro(true);
|
||||
activityTestRule.launchActivity(new Intent());
|
||||
BaristaSleepInteractions.sleep(TEST_PAUSE_MS);
|
||||
|
||||
// Verify that the SplashActivity is followed by IntroActivity
|
||||
@ -78,11 +80,9 @@ public class SplashActivityTest {
|
||||
|
||||
@Test
|
||||
public void testNormalRun() {
|
||||
SharedPreferences.Editor preferencesEditor =
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
preferencesEditor.putBoolean(PREF_SHOW_INTRO, false).apply();
|
||||
shouldShowIntro(false);
|
||||
|
||||
activityTestRule.launchActivity();
|
||||
activityTestRule.launchActivity(new Intent());
|
||||
BaristaSleepInteractions.sleep(TEST_PAUSE_MS);
|
||||
|
||||
// Verify that the SplashActivity is followed by MainActivity
|
||||
@ -93,4 +93,10 @@ public class SplashActivityTest {
|
||||
public void endTest() {
|
||||
Intents.release();
|
||||
}
|
||||
|
||||
private void shouldShowIntro(boolean value) {
|
||||
SharedPreferences.Editor preferencesEditor =
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit();
|
||||
preferencesEditor.putBoolean(PREF_SHOW_INTRO, value).apply();
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ class ZimManageActivityTest : BaseActivityTest<ZimManageActivity>() {
|
||||
searchFor(book)
|
||||
pressBack()
|
||||
pressBack()
|
||||
queueMockResponseWith("0123456789")
|
||||
forceResponse("0123456789")
|
||||
clickOn(book)
|
||||
}
|
||||
clickOnDownloading {
|
||||
@ -51,7 +51,7 @@ class ZimManageActivityTest : BaseActivityTest<ZimManageActivity>() {
|
||||
clickPositiveDialogButton()
|
||||
}
|
||||
clickOnOnline {
|
||||
queueMockResponseWith("01234")
|
||||
forceResponse("01234")
|
||||
clickOn(book)
|
||||
}
|
||||
clickOnDownloading {
|
||||
@ -72,8 +72,8 @@ class ZimManageActivityTest : BaseActivityTest<ZimManageActivity>() {
|
||||
} clickOnLanguageIcon { }
|
||||
}
|
||||
|
||||
private fun queueMockResponseWith(body: String) {
|
||||
mockServer.queueResponse(
|
||||
private fun forceResponse(body: String) {
|
||||
mockServer.forceResponse(
|
||||
MockResponse()
|
||||
.setBody(body)
|
||||
.throttleBody(
|
||||
|
@ -1,15 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:installLocation="auto"
|
||||
package="org.kiwix.kiwixmobile">
|
||||
package="org.kiwix.kiwixmobile"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
<!-- Devices with version >= Oreo need location permission to start/stop the hotspot -->
|
||||
@ -147,7 +147,6 @@
|
||||
android:name=".zim_manager.ZimManageActivity"
|
||||
android:label="@string/choose_file"
|
||||
android:launchMode="singleTop">
|
||||
|
||||
<!-- TODO -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.GET_CONTENT" />
|
||||
@ -228,12 +227,5 @@
|
||||
<data android:host="*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<receiver android:name=".zim_manager.DownloadNotificationClickedReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
@ -51,6 +51,7 @@ internal object ExternalPaths {
|
||||
"/mnt/extsd",
|
||||
"/extsd",
|
||||
"/mnt/sdcard",
|
||||
"/misc/android"
|
||||
"/misc/android",
|
||||
"/mnt"
|
||||
)
|
||||
}
|
||||
|
@ -19,67 +19,12 @@
|
||||
|
||||
package eu.mhutti1.utils.storage
|
||||
|
||||
import android.util.Log
|
||||
import java.io.BufferedReader
|
||||
import java.io.File
|
||||
import java.io.FileReader
|
||||
import java.io.FileWriter
|
||||
import java.io.IOException
|
||||
|
||||
const val LOCATION_EXTENSION = "storageLocationMarker"
|
||||
|
||||
data class StorageDevice(val file: File, val isInternal: Boolean) {
|
||||
|
||||
constructor(path: String, internal: Boolean) : this(File(path), internal)
|
||||
|
||||
init {
|
||||
if (file.exists()) {
|
||||
createLocationCode()
|
||||
}
|
||||
}
|
||||
|
||||
var isDuplicate = false
|
||||
private set
|
||||
|
||||
val name: String
|
||||
get() = file.path
|
||||
|
||||
// Create unique file to identify duplicate devices.
|
||||
private fun createLocationCode() {
|
||||
if (!getLocationCodeFromFolder(file)) {
|
||||
File(file, ".$LOCATION_EXTENSION").let { locationCode ->
|
||||
try {
|
||||
locationCode.createNewFile()
|
||||
FileWriter(locationCode).use { it.write(file.path) }
|
||||
} catch (ioException: IOException) {
|
||||
Log.d("StorageDevice", "could not write file $file", ioException)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there is already a device code in our path
|
||||
private fun getLocationCodeFromFolder(folder: File): Boolean {
|
||||
val locationCode = File(folder, ".$LOCATION_EXTENSION")
|
||||
if (locationCode.exists()) {
|
||||
try {
|
||||
BufferedReader(FileReader(locationCode)).use { br ->
|
||||
if (br.readLine() == file.path) {
|
||||
isDuplicate = false
|
||||
} else {
|
||||
isDuplicate = true
|
||||
return@getLocationCodeFromFolder true
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
val parent = folder.parentFile
|
||||
if (parent == null) {
|
||||
isDuplicate = false
|
||||
return false
|
||||
}
|
||||
return getLocationCodeFromFolder(parent)
|
||||
}
|
||||
}
|
||||
|
@ -28,15 +28,17 @@ import java.io.RandomAccessFile
|
||||
import java.util.ArrayList
|
||||
|
||||
object StorageDeviceUtils {
|
||||
@JvmStatic
|
||||
fun getWritableStorage(context: Context) = validate(externalFilesDirsDevices(context, true), true)
|
||||
|
||||
@JvmStatic
|
||||
fun getStorageDevices(context: Context, writable: Boolean): List<StorageDevice> {
|
||||
fun getReadableStorage(context: Context): List<StorageDevice> {
|
||||
val storageDevices = ArrayList<StorageDevice>().apply {
|
||||
add(environmentDevices(writable))
|
||||
add(environmentDevices())
|
||||
addAll(externalMountPointDevices())
|
||||
addAll(externalFilesDirsDevices(context, writable))
|
||||
addAll(externalFilesDirsDevices(context, false))
|
||||
}
|
||||
return validate(storageDevices, writable)
|
||||
return validate(storageDevices, false)
|
||||
}
|
||||
|
||||
private fun externalFilesDirsDevices(
|
||||
@ -44,7 +46,7 @@ object StorageDeviceUtils {
|
||||
writable: Boolean
|
||||
) = ContextCompat.getExternalFilesDirs(context, "")
|
||||
.filterNotNull()
|
||||
.map { dir -> StorageDevice(generalisePath(dir.path, writable), false) }
|
||||
.mapIndexed { index, dir -> StorageDevice(generalisePath(dir.path, writable), index == 0) }
|
||||
|
||||
private fun externalMountPointDevices(): Collection<StorageDevice> =
|
||||
ExternalPaths.possiblePaths.fold(mutableListOf(), { acc, path ->
|
||||
@ -62,11 +64,9 @@ object StorageDeviceUtils {
|
||||
?.map { dir -> StorageDevice(dir, false) }
|
||||
.orEmpty()
|
||||
|
||||
private fun environmentDevices(
|
||||
writable: Boolean
|
||||
) =
|
||||
private fun environmentDevices() =
|
||||
StorageDevice(
|
||||
generalisePath(Environment.getExternalStorageDirectory().path, writable),
|
||||
generalisePath(Environment.getExternalStorageDirectory().path, false),
|
||||
Environment.isExternalStorageEmulated()
|
||||
)
|
||||
|
||||
@ -94,26 +94,12 @@ object StorageDeviceUtils {
|
||||
}
|
||||
|
||||
private fun validate(
|
||||
storageDevices: ArrayList<StorageDevice>,
|
||||
storageDevices: List<StorageDevice>,
|
||||
writable: Boolean
|
||||
) = storageDevices.asSequence().distinct()
|
||||
) = storageDevices.asSequence()
|
||||
.filter { it.file.exists() }
|
||||
.filter { it.file.isDirectory }
|
||||
.filter { canWrite(it.file) || !writable }
|
||||
.filterNot(StorageDevice::isDuplicate)
|
||||
.distinctBy { it.file.canonicalPath }
|
||||
.filter { !writable || canWrite(it.file) }
|
||||
.toList()
|
||||
.also(StorageDeviceUtils::deleteStorageMarkers)
|
||||
|
||||
private fun deleteStorageMarkers(validatedDevices: List<StorageDevice>) {
|
||||
validatedDevices.forEach { recursiveDeleteStorageMarkers(it.file) }
|
||||
}
|
||||
|
||||
private fun recursiveDeleteStorageMarkers(file: File) {
|
||||
file.listFiles().forEach {
|
||||
when {
|
||||
it.isDirectory -> recursiveDeleteStorageMarkers(it)
|
||||
it.extension == LOCATION_EXTENSION -> it.delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ class StorageSelectDialog : DialogFragment() {
|
||||
title.text = aTitle
|
||||
adapter = StorageSelectArrayAdapter(
|
||||
activity!!,
|
||||
StorageDeviceUtils.getStorageDevices(activity!!, true),
|
||||
StorageDeviceUtils.getWritableStorage(activity!!),
|
||||
storageCalculator
|
||||
)
|
||||
device_list.adapter = adapter
|
||||
|
@ -35,6 +35,7 @@ import javax.inject.Inject;
|
||||
import org.kiwix.kiwixmobile.data.local.KiwixDatabase;
|
||||
import org.kiwix.kiwixmobile.di.components.ApplicationComponent;
|
||||
import org.kiwix.kiwixmobile.di.components.DaggerApplicationComponent;
|
||||
import org.kiwix.kiwixmobile.downloader.DownloadMonitor;
|
||||
|
||||
public class KiwixApplication extends MultiDexApplication implements HasActivityInjector {
|
||||
|
||||
@ -48,6 +49,8 @@ public class KiwixApplication extends MultiDexApplication implements HasActivity
|
||||
@Inject
|
||||
DispatchingAndroidInjector<Activity> activityInjector;
|
||||
@Inject
|
||||
DownloadMonitor downloadMonitor;
|
||||
@Inject
|
||||
KiwixDatabase kiwixDatabase;
|
||||
|
||||
public static KiwixApplication getInstance() {
|
||||
@ -78,6 +81,7 @@ public class KiwixApplication extends MultiDexApplication implements HasActivity
|
||||
writeLogFile();
|
||||
applicationComponent.inject(this);
|
||||
kiwixDatabase.forceMigration();
|
||||
downloadMonitor.init();
|
||||
if (BuildConfig.DEBUG) {
|
||||
StrictMode.setThreadPolicy(buildThreadPolicy(new StrictMode.ThreadPolicy.Builder()));
|
||||
StrictMode.setVmPolicy(buildVmPolicy(new StrictMode.VmPolicy.Builder()));
|
||||
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (C) 2018 Kiwix <android.kiwix.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.database.newdb.dao
|
||||
|
||||
import com.tonyodev.fetch2.Download
|
||||
import com.tonyodev.fetch2.Status.COMPLETED
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.kotlin.equal
|
||||
import io.objectbox.kotlin.query
|
||||
import io.reactivex.Flowable
|
||||
import org.kiwix.kiwixmobile.database.newdb.entities.FetchDownloadEntity
|
||||
import org.kiwix.kiwixmobile.database.newdb.entities.FetchDownloadEntity_
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
|
||||
import javax.inject.Inject
|
||||
|
||||
class FetchDownloadDao @Inject constructor(
|
||||
private val box: Box<FetchDownloadEntity>,
|
||||
private val newBookDao: NewBookDao
|
||||
) {
|
||||
|
||||
fun downloads(): Flowable<List<DownloadModel>> =
|
||||
box.asFlowable()
|
||||
.distinctUntilChanged()
|
||||
.doOnNext(::moveCompletedToBooksOnDiskDao)
|
||||
.map { it.map(::DownloadModel) }
|
||||
|
||||
private fun moveCompletedToBooksOnDiskDao(downloadEntities: List<FetchDownloadEntity>) {
|
||||
downloadEntities.filter { it.status == COMPLETED }.takeIf { it.isNotEmpty() }?.let {
|
||||
box.remove(it)
|
||||
newBookDao.insert(it.map(::BookOnDisk))
|
||||
}
|
||||
}
|
||||
|
||||
fun update(download: Download) {
|
||||
box.store.callInTx {
|
||||
getEntityFor(download)?.let { dbEntity ->
|
||||
dbEntity.updateWith(download)
|
||||
.takeIf { updatedEntity -> updatedEntity != dbEntity }
|
||||
?.let(box::put)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEntityFor(download: Download) =
|
||||
box.query {
|
||||
equal(FetchDownloadEntity_.downloadId, download.id)
|
||||
}.find().getOrNull(0)
|
||||
|
||||
fun doesNotAlreadyExist(book: Book) =
|
||||
box.query {
|
||||
equal(FetchDownloadEntity_.bookId, book.id)
|
||||
}.count() == 0L
|
||||
|
||||
fun insert(downloadId: Long, book: Book) {
|
||||
box.put(FetchDownloadEntity(downloadId, book))
|
||||
}
|
||||
|
||||
fun delete(download: Download) {
|
||||
box.query {
|
||||
equal(FetchDownloadEntity_.downloadId, download.id)
|
||||
}.remove()
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package org.kiwix.kiwixmobile.database.newdb.dao
|
||||
|
||||
import io.objectbox.Box
|
||||
import io.objectbox.kotlin.inValues
|
||||
import io.objectbox.kotlin.query
|
||||
import org.kiwix.kiwixmobile.database.newdb.entities.DownloadEntity
|
||||
import org.kiwix.kiwixmobile.database.newdb.entities.DownloadEntity_
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
|
||||
import javax.inject.Inject
|
||||
|
||||
class NewDownloadDao @Inject constructor(private val box: Box<DownloadEntity>) {
|
||||
|
||||
fun downloads() = box.asFlowable()
|
||||
.map { it.map(DownloadEntity::toDownloadModel) }
|
||||
|
||||
fun delete(vararg downloadIds: Long) {
|
||||
box
|
||||
.query {
|
||||
inValues(DownloadEntity_.downloadId, downloadIds)
|
||||
}
|
||||
.remove()
|
||||
}
|
||||
|
||||
fun containsAny(vararg downloadIds: Long) =
|
||||
box
|
||||
.query {
|
||||
inValues(DownloadEntity_.downloadId, downloadIds)
|
||||
}
|
||||
.count() > 0
|
||||
|
||||
fun doesNotAlreadyExist(book: Book) =
|
||||
box
|
||||
.query {
|
||||
equal(DownloadEntity_.bookId, book.id)
|
||||
}
|
||||
.count() == 0L
|
||||
|
||||
fun insert(downloadModel: DownloadModel) {
|
||||
box.put(DownloadEntity(downloadModel))
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (C) 2018 Kiwix <android.kiwix.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.database.newdb.entities
|
||||
|
||||
import io.objectbox.annotation.Entity
|
||||
import io.objectbox.annotation.Id
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
|
||||
|
||||
@Entity
|
||||
data class DownloadEntity(
|
||||
@Id var id: Long = 0,
|
||||
val downloadId: Long,
|
||||
val bookId: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val language: String,
|
||||
val creator: String,
|
||||
val publisher: String,
|
||||
val date: String,
|
||||
val url: String?,
|
||||
val articleCount: String?,
|
||||
val mediaCount: String?,
|
||||
val size: String,
|
||||
val name: String?,
|
||||
val favIcon: String
|
||||
) {
|
||||
constructor(downloadModel: DownloadModel) : this(
|
||||
0,
|
||||
downloadModel.downloadId,
|
||||
downloadModel.book.getId(),
|
||||
downloadModel.book.getTitle(),
|
||||
downloadModel.book.getDescription(),
|
||||
downloadModel.book.getLanguage(),
|
||||
downloadModel.book.getCreator(),
|
||||
downloadModel.book.getPublisher(),
|
||||
downloadModel.book.getDate(),
|
||||
downloadModel.book.getUrl(),
|
||||
downloadModel.book.getArticleCount(),
|
||||
downloadModel.book.getMediaCount(),
|
||||
downloadModel.book.getSize(),
|
||||
downloadModel.book.name,
|
||||
downloadModel.book.getFavicon()
|
||||
)
|
||||
|
||||
fun toDownloadModel() = DownloadModel(id, downloadId, toBook())
|
||||
|
||||
private fun toBook() = Book().apply {
|
||||
id = bookId
|
||||
title = this@DownloadEntity.title
|
||||
description = this@DownloadEntity.description
|
||||
language = this@DownloadEntity.language
|
||||
creator = this@DownloadEntity.creator
|
||||
publisher = this@DownloadEntity.publisher
|
||||
date = this@DownloadEntity.date
|
||||
url = this@DownloadEntity.url
|
||||
articleCount = this@DownloadEntity.articleCount
|
||||
mediaCount = this@DownloadEntity.mediaCount
|
||||
size = this@DownloadEntity.size
|
||||
bookName = name
|
||||
favicon = favIcon
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (C) 2018 Kiwix <android.kiwix.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.database.newdb.entities
|
||||
|
||||
import com.tonyodev.fetch2.Download
|
||||
import com.tonyodev.fetch2.Error
|
||||
import com.tonyodev.fetch2.Status
|
||||
import io.objectbox.annotation.Convert
|
||||
import io.objectbox.annotation.Entity
|
||||
import io.objectbox.annotation.Id
|
||||
import io.objectbox.converter.PropertyConverter
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
|
||||
|
||||
@Entity
|
||||
data class FetchDownloadEntity(
|
||||
@Id var id: Long = 0,
|
||||
var downloadId: Long,
|
||||
val file: String? = null,
|
||||
val etaInMilliSeconds: Long = -1L,
|
||||
val bytesDownloaded: Long = -1L,
|
||||
val totalSizeOfDownload: Long = -1L,
|
||||
@Convert(converter = StatusConverter::class, dbType = Int::class)
|
||||
val status: Status = Status.NONE,
|
||||
@Convert(converter = ErrorConverter::class, dbType = Int::class)
|
||||
val error: Error = Error.NONE,
|
||||
val progress: Int = -1,
|
||||
val bookId: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val language: String,
|
||||
val creator: String,
|
||||
val publisher: String,
|
||||
val date: String,
|
||||
val url: String?,
|
||||
val articleCount: String?,
|
||||
val mediaCount: String?,
|
||||
val size: String,
|
||||
val name: String?,
|
||||
val favIcon: String
|
||||
) {
|
||||
constructor(downloadId: Long, book: Book) : this(
|
||||
downloadId = downloadId,
|
||||
bookId = book.getId(),
|
||||
title = book.getTitle(),
|
||||
description = book.getDescription(),
|
||||
language = book.getLanguage(),
|
||||
creator = book.getCreator(),
|
||||
publisher = book.getPublisher(),
|
||||
date = book.getDate(),
|
||||
url = book.getUrl(),
|
||||
articleCount = book.getArticleCount(),
|
||||
mediaCount = book.getMediaCount(),
|
||||
size = book.getSize(),
|
||||
name = book.name,
|
||||
favIcon = book.getFavicon()
|
||||
)
|
||||
|
||||
fun toBook() = Book().apply {
|
||||
id = bookId
|
||||
title = this@FetchDownloadEntity.title
|
||||
description = this@FetchDownloadEntity.description
|
||||
language = this@FetchDownloadEntity.language
|
||||
creator = this@FetchDownloadEntity.creator
|
||||
publisher = this@FetchDownloadEntity.publisher
|
||||
date = this@FetchDownloadEntity.date
|
||||
url = this@FetchDownloadEntity.url
|
||||
articleCount = this@FetchDownloadEntity.articleCount
|
||||
mediaCount = this@FetchDownloadEntity.mediaCount
|
||||
size = this@FetchDownloadEntity.size
|
||||
bookName = name
|
||||
favicon = favIcon
|
||||
}
|
||||
|
||||
fun updateWith(download: Download) = copy(
|
||||
file = download.file,
|
||||
etaInMilliSeconds = download.etaInMilliSeconds,
|
||||
bytesDownloaded = download.downloaded,
|
||||
totalSizeOfDownload = download.total,
|
||||
status = download.status,
|
||||
error = download.error,
|
||||
progress = download.progress
|
||||
)
|
||||
}
|
||||
|
||||
class StatusConverter : EnumConverter<Status>() {
|
||||
override fun convertToEntityProperty(databaseValue: Int) = Status.valueOf(databaseValue)
|
||||
}
|
||||
|
||||
class ErrorConverter : EnumConverter<Error>() {
|
||||
override fun convertToEntityProperty(databaseValue: Int) = Error.valueOf(databaseValue)
|
||||
}
|
||||
|
||||
abstract class EnumConverter<E : Enum<E>> : PropertyConverter<E, Int> {
|
||||
override fun convertToDatabaseValue(entityProperty: E): Int = entityProperty.ordinal
|
||||
}
|
@ -29,12 +29,10 @@ import org.kiwix.kiwixmobile.data.ZimContentProvider;
|
||||
import org.kiwix.kiwixmobile.di.modules.ApplicationModule;
|
||||
import org.kiwix.kiwixmobile.di.modules.JNIModule;
|
||||
import org.kiwix.kiwixmobile.di.modules.NetworkModule;
|
||||
import org.kiwix.kiwixmobile.downloader.DownloadService;
|
||||
import org.kiwix.kiwixmobile.language.LanguageActivity;
|
||||
import org.kiwix.kiwixmobile.main.KiwixWebView;
|
||||
import org.kiwix.kiwixmobile.search.AutoCompleteAdapter;
|
||||
import org.kiwix.kiwixmobile.settings.PrefsFragment;
|
||||
import org.kiwix.kiwixmobile.zim_manager.DownloadNotificationClickedReceiver;
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageActivity;
|
||||
|
||||
@Singleton
|
||||
@ -60,8 +58,6 @@ public interface ApplicationComponent {
|
||||
|
||||
void inject(KiwixApplication application);
|
||||
|
||||
void inject(DownloadService service);
|
||||
|
||||
void inject(ZimContentProvider zimContentProvider);
|
||||
|
||||
void inject(KiwixWebView kiwixWebView);
|
||||
@ -70,8 +66,6 @@ public interface ApplicationComponent {
|
||||
|
||||
void inject(AutoCompleteAdapter autoCompleteAdapter);
|
||||
|
||||
void inject(DownloadNotificationClickedReceiver downloadNotificationClickedReceiver);
|
||||
|
||||
void inject(@NotNull ZimManageActivity zimManageActivity);
|
||||
|
||||
void inject(@NotNull LanguageActivity languageActivity);
|
||||
|
@ -33,7 +33,8 @@ import javax.inject.Singleton;
|
||||
import org.kiwix.kiwixmobile.di.qualifiers.Computation;
|
||||
import org.kiwix.kiwixmobile.di.qualifiers.IO;
|
||||
import org.kiwix.kiwixmobile.di.qualifiers.MainThread;
|
||||
import org.kiwix.kiwixmobile.downloader.model.UriToFileConverter;
|
||||
import org.kiwix.kiwixmobile.downloader.DownloadMonitor;
|
||||
import org.kiwix.kiwixmobile.downloader.fetch.FetchDownloadMonitor;
|
||||
import org.kiwix.kiwixmobile.utils.BookUtils;
|
||||
|
||||
@Module(includes = {
|
||||
@ -84,13 +85,13 @@ public class ApplicationModule {
|
||||
}
|
||||
|
||||
@Provides @Singleton
|
||||
UriToFileConverter provideUriToFIleCOnverter() {
|
||||
return new UriToFileConverter.Impl();
|
||||
LocationManager provideLocationManager(Context context) {
|
||||
return (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
}
|
||||
|
||||
@Provides @Singleton
|
||||
LocationManager provideLocationManager(Context context) {
|
||||
return (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
DownloadMonitor provideDownloadMonitor(FetchDownloadMonitor fetchDownloadMonitor) {
|
||||
return fetchDownloadMonitor;
|
||||
}
|
||||
|
||||
@Provides @Singleton
|
||||
|
@ -22,31 +22,29 @@ import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.objectbox.BoxStore
|
||||
import io.objectbox.kotlin.boxFor
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.FetchDownloadDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.HistoryDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewBookDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewBookmarksDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewDownloadDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewLanguagesDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewRecentSearchDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.entities.MyObjectBox
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
class DatabaseModule {
|
||||
open class DatabaseModule {
|
||||
companion object {
|
||||
var boxStore: BoxStore? = null
|
||||
}
|
||||
|
||||
// NOT RECOMMENDED TODO use custom runner to load TestApplication
|
||||
@Provides @Singleton fun providesBoxStore(context: Context): BoxStore {
|
||||
if (boxStore == null) {
|
||||
boxStore = MyObjectBox.builder().androidContext(context.applicationContext).build()
|
||||
boxStore = MyObjectBox.builder().androidContext(context).build()
|
||||
}
|
||||
return boxStore!!
|
||||
}
|
||||
|
||||
@Provides @Singleton fun providesNewDownloadDao(boxStore: BoxStore): NewDownloadDao =
|
||||
NewDownloadDao(boxStore.boxFor())
|
||||
|
||||
@Provides @Singleton fun providesNewBookDao(boxStore: BoxStore): NewBookDao =
|
||||
NewBookDao(boxStore.boxFor())
|
||||
|
||||
@ -61,4 +59,10 @@ class DatabaseModule {
|
||||
|
||||
@Provides @Singleton fun providesNewRecentSearchDao(boxStore: BoxStore): NewRecentSearchDao =
|
||||
NewRecentSearchDao(boxStore.boxFor())
|
||||
|
||||
@Provides @Singleton fun providesFetchDownloadDao(
|
||||
boxStore: BoxStore,
|
||||
newBookDao: NewBookDao
|
||||
): FetchDownloadDao =
|
||||
FetchDownloadDao(boxStore.boxFor(), newBookDao)
|
||||
}
|
||||
|
@ -17,18 +17,75 @@
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.di.modules
|
||||
|
||||
import dagger.Binds
|
||||
import android.content.Context
|
||||
import com.tonyodev.fetch2.Fetch
|
||||
import com.tonyodev.fetch2.Fetch.Impl
|
||||
import com.tonyodev.fetch2.FetchConfiguration
|
||||
import com.tonyodev.fetch2.FetchNotificationManager
|
||||
import com.tonyodev.fetch2okhttp.OkHttpDownloader
|
||||
import dagger.Module
|
||||
import org.kiwix.kiwixmobile.downloader.DownloadManagerRequester
|
||||
import dagger.Provides
|
||||
import okhttp3.OkHttpClient
|
||||
import org.kiwix.kiwixmobile.BuildConfig
|
||||
import org.kiwix.kiwixmobile.data.remote.KiwixService
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.FetchDownloadDao
|
||||
import org.kiwix.kiwixmobile.downloader.DownloadRequester
|
||||
import org.kiwix.kiwixmobile.downloader.Downloader
|
||||
import org.kiwix.kiwixmobile.downloader.DownloaderImpl
|
||||
import org.kiwix.kiwixmobile.downloader.fetch.FetchDownloadNotificationManager
|
||||
import org.kiwix.kiwixmobile.downloader.fetch.FetchDownloadRequester
|
||||
import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
abstract class DownloaderModule {
|
||||
@Binds
|
||||
abstract fun bindDownloader(downloaderImpl: DownloaderImpl): Downloader
|
||||
object DownloaderModule {
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesDownloader(
|
||||
downloadRequester: DownloadRequester,
|
||||
downloadDao: FetchDownloadDao,
|
||||
kiwixService: KiwixService
|
||||
): Downloader = DownloaderImpl(downloadRequester, downloadDao, kiwixService)
|
||||
|
||||
@Binds
|
||||
abstract fun bindDownloaderRequester(downloaderImpl: DownloadManagerRequester): DownloadRequester
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesDownloadRequester(
|
||||
fetch: Fetch,
|
||||
sharedPreferenceUtil: SharedPreferenceUtil
|
||||
): DownloadRequester = FetchDownloadRequester(fetch, sharedPreferenceUtil)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFetch(fetchConfiguration: FetchConfiguration): Fetch =
|
||||
Fetch.getInstance(fetchConfiguration)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFetchConfiguration(
|
||||
context: Context,
|
||||
okHttpDownloader: OkHttpDownloader,
|
||||
fetchNotificationManager: FetchNotificationManager
|
||||
): FetchConfiguration =
|
||||
FetchConfiguration.Builder(context).apply {
|
||||
setDownloadConcurrentLimit(5)
|
||||
enableLogging(BuildConfig.DEBUG)
|
||||
enableRetryOnNetworkGain(true)
|
||||
setHttpDownloader(okHttpDownloader)
|
||||
setNotificationManager(fetchNotificationManager)
|
||||
}.build().also(Impl::setDefaultInstanceConfiguration)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpDownloader() = OkHttpDownloader(OkHttpClient.Builder().build())
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFetchDownloadNotificationManager(context: Context): FetchNotificationManager =
|
||||
FetchDownloadNotificationManager(context)
|
||||
}
|
||||
|
@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (C) 2018 Kiwix <android.kiwix.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.downloader
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.app.DownloadManager.Request
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadItem
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadRequest
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadStatus
|
||||
import org.kiwix.kiwixmobile.extensions.forEachRow
|
||||
import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil
|
||||
import org.kiwix.kiwixmobile.utils.StorageUtils
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class DownloadManagerRequester @Inject constructor(
|
||||
private val downloadManager: DownloadManager,
|
||||
private val sharedPreferenceUtil: SharedPreferenceUtil
|
||||
) : DownloadRequester {
|
||||
|
||||
override fun enqueue(downloadRequest: DownloadRequest) =
|
||||
downloadManager.enqueue(downloadRequest.toDownloadManagerRequest(sharedPreferenceUtil))
|
||||
|
||||
override fun query(downloadModels: List<DownloadModel>): List<DownloadStatus> {
|
||||
val downloadStatuses = mutableListOf<DownloadStatus>()
|
||||
if (downloadModels.isNotEmpty()) {
|
||||
downloadModels.forEach { model ->
|
||||
downloadManager.query(model.toQuery())
|
||||
.forEachRow {
|
||||
downloadStatuses.add(DownloadStatus(it, model))
|
||||
}
|
||||
}
|
||||
}
|
||||
return downloadStatuses
|
||||
}
|
||||
|
||||
override fun cancel(downloadItem: DownloadItem) {
|
||||
downloadManager.remove(downloadItem.downloadId)
|
||||
}
|
||||
|
||||
private fun DownloadRequest.toDownloadManagerRequest(sharedPreferenceUtil: SharedPreferenceUtil) =
|
||||
Request(uri).apply {
|
||||
setAllowedNetworkTypes(
|
||||
if (sharedPreferenceUtil.prefWifiOnly) {
|
||||
Request.NETWORK_WIFI
|
||||
} else {
|
||||
Request.NETWORK_MOBILE or Request.NETWORK_WIFI
|
||||
}
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
setAllowedOverMetered(true)
|
||||
}
|
||||
setAllowedOverRoaming(true)
|
||||
setTitle(title)
|
||||
setDescription(description)
|
||||
setDestinationUri(toDestinationUri(sharedPreferenceUtil))
|
||||
setNotificationVisibility(Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
setVisibleInDownloadsUi(true)
|
||||
}
|
||||
|
||||
private fun DownloadRequest.toDestinationUri(sharedPreferenceUtil: SharedPreferenceUtil) =
|
||||
Uri.fromFile(
|
||||
File(
|
||||
"${sharedPreferenceUtil.prefStorage}/Kiwix/${
|
||||
StorageUtils.getFileNameFromUrl(urlString)
|
||||
}"
|
||||
)
|
||||
)
|
||||
|
||||
private fun DownloadModel.toQuery() =
|
||||
DownloadManager.Query().setFilterById(downloadId)
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (C) 2018 Kiwix <android.kiwix.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.downloader
|
||||
|
||||
interface DownloadMonitor {
|
||||
fun init()
|
||||
}
|
@ -18,12 +18,9 @@
|
||||
package org.kiwix.kiwixmobile.downloader
|
||||
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadItem
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadRequest
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadStatus
|
||||
|
||||
interface DownloadRequester {
|
||||
fun enqueue(downloadRequest: DownloadRequest): Long
|
||||
fun query(downloadModels: List<DownloadModel>): List<DownloadStatus>
|
||||
fun cancel(downloadItem: DownloadItem)
|
||||
}
|
||||
|
@ -1,719 +0,0 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (C) 2018 Kiwix <android.kiwix.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.downloader;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.util.SparseArray;
|
||||
import android.util.SparseIntArray;
|
||||
import android.widget.Toast;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Observer;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.inject.Inject;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okio.BufferedSource;
|
||||
import org.kiwix.kiwixmobile.KiwixApplication;
|
||||
import org.kiwix.kiwixmobile.R;
|
||||
import org.kiwix.kiwixmobile.data.DataSource;
|
||||
import org.kiwix.kiwixmobile.data.remote.KiwixService;
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewBookDao;
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity;
|
||||
import org.kiwix.kiwixmobile.main.MainActivity;
|
||||
import org.kiwix.kiwixmobile.utils.Constants;
|
||||
import org.kiwix.kiwixmobile.utils.NetworkUtils;
|
||||
import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil;
|
||||
import org.kiwix.kiwixmobile.utils.StorageUtils;
|
||||
import org.kiwix.kiwixmobile.utils.TestingUtils;
|
||||
import org.kiwix.kiwixmobile.utils.files.FileUtils;
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageActivity;
|
||||
|
||||
import static org.kiwix.kiwixmobile.downloader.ChunkUtils.ALPHABET;
|
||||
import static org.kiwix.kiwixmobile.downloader.ChunkUtils.PART;
|
||||
import static org.kiwix.kiwixmobile.downloader.ChunkUtils.ZIM_EXTENSION;
|
||||
import static org.kiwix.kiwixmobile.utils.Constants.EXTRA_BOOK;
|
||||
import static org.kiwix.kiwixmobile.utils.Constants.EXTRA_LIBRARY;
|
||||
import static org.kiwix.kiwixmobile.utils.Constants.EXTRA_NOTIFICATION_ID;
|
||||
import static org.kiwix.kiwixmobile.utils.Constants.EXTRA_ZIM_FILE;
|
||||
import static org.kiwix.kiwixmobile.utils.Constants.ONGOING_DOWNLOAD_CHANNEL_ID;
|
||||
|
||||
@Deprecated
|
||||
public class DownloadService extends Service {
|
||||
|
||||
public static final int PLAY = 1;
|
||||
public static final int PAUSE = 2;
|
||||
public static final int FINISH = 3;
|
||||
public static final int CANCEL = 4;
|
||||
public static final String ACTION_PAUSE = "PAUSE";
|
||||
public static final String ACTION_STOP = "STOP";
|
||||
public static final String ACTION_NO_WIFI = "NO_WIFI";
|
||||
public static final String NOTIFICATION_ID = "NOTIFICATION_ID";
|
||||
public static final String NOTIFICATION_TITLE_KEY = "NOTIFICATION_TITLE_KEY";
|
||||
public static final Object pauseLock = new Object();
|
||||
// 1024 / 100
|
||||
private static final double BOOK_SIZE_OFFSET = 10.24;
|
||||
private static final String KIWIX_TAG = "kiwixdownloadservice";
|
||||
public static String KIWIX_ROOT;
|
||||
public static ArrayList<String> notifications = new ArrayList<>();
|
||||
private static String SD_CARD;
|
||||
private static DownloadFragment downloadFragment;
|
||||
private final IBinder mBinder = new LocalBinder();
|
||||
public String notificationTitle;
|
||||
public SparseIntArray downloadStatus = new SparseIntArray();
|
||||
public SparseIntArray downloadProgress = new SparseIntArray();
|
||||
public SparseIntArray timeRemaining = new SparseIntArray();
|
||||
@Inject
|
||||
KiwixService kiwixService;
|
||||
@Inject
|
||||
OkHttpClient httpClient;
|
||||
@Inject
|
||||
NotificationManager notificationManager;
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@Inject
|
||||
SharedPreferenceUtil sharedPreferenceUtil;
|
||||
|
||||
@Inject
|
||||
NewBookDao bookDao;
|
||||
@Inject
|
||||
DataSource dataSource;
|
||||
private SparseArray<NotificationCompat.Builder> notification = new SparseArray<>();
|
||||
|
||||
public static void setDownloadFragment(DownloadFragment dFragment) {
|
||||
downloadFragment = dFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
KiwixApplication.getApplicationComponent().inject(this);
|
||||
|
||||
SD_CARD = sharedPreferenceUtil.getPrefStorage();
|
||||
KIWIX_ROOT = SD_CARD + "/Kiwix/";
|
||||
|
||||
KIWIX_ROOT = checkWritable(KIWIX_ROOT);
|
||||
|
||||
createOngoingDownloadChannel();
|
||||
|
||||
super.onCreate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent == null) {
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
String log = intent.getAction() + " : ";
|
||||
if (intent.hasExtra(NOTIFICATION_ID)) {
|
||||
log += intent.getIntExtra(NOTIFICATION_ID, -3);
|
||||
}
|
||||
Log.d(KIWIX_TAG, log);
|
||||
if (intent.hasExtra(NOTIFICATION_ID) && intent.getAction().equals(ACTION_STOP)) {
|
||||
stopDownload(intent.getIntExtra(NOTIFICATION_ID, 0));
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
if (intent.hasExtra(NOTIFICATION_ID) && (intent.getAction().equals(ACTION_PAUSE))) {
|
||||
if (MainActivity.wifiOnly && !NetworkUtils.isWiFi(getApplicationContext())) {
|
||||
Log.i(KIWIX_TAG, "Not connected to WiFi, and wifiOnly is enabled");
|
||||
startActivity(new Intent(this, ZimManageActivity.class).setAction(ACTION_NO_WIFI)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
this.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
|
||||
} else {
|
||||
toggleDownload(intent.getIntExtra(NOTIFICATION_ID, 0));
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
SD_CARD = sharedPreferenceUtil.getPrefStorage();
|
||||
KIWIX_ROOT = SD_CARD + "/Kiwix/";
|
||||
|
||||
KIWIX_ROOT = checkWritable(KIWIX_ROOT);
|
||||
|
||||
Log.d(KIWIX_TAG, "Using KIWIX_ROOT: " + KIWIX_ROOT);
|
||||
|
||||
notificationTitle = intent.getExtras().getString(DownloadIntent.DOWNLOAD_ZIM_TITLE);
|
||||
LibraryNetworkEntity.Book book =
|
||||
(LibraryNetworkEntity.Book) intent.getSerializableExtra(EXTRA_BOOK);
|
||||
int notificationID = book.getId().hashCode();
|
||||
|
||||
if (downloadStatus.get(notificationID, -1) == PAUSE
|
||||
|| downloadStatus.get(notificationID, -1) == PLAY) {
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
notifications.add(notificationTitle);
|
||||
final Intent target = new Intent(this, MainActivity.class);
|
||||
target.putExtra(EXTRA_LIBRARY, true);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity
|
||||
(getBaseContext(), notificationID,
|
||||
target, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
Intent pauseIntent = new Intent(this, this.getClass()).setAction(ACTION_PAUSE)
|
||||
.putExtra(NOTIFICATION_ID, notificationID);
|
||||
Intent stopIntent = new Intent(this, this.getClass()).setAction(ACTION_STOP)
|
||||
.putExtra(NOTIFICATION_ID, notificationID);
|
||||
PendingIntent pausePending =
|
||||
PendingIntent.getService(getBaseContext(), notificationID, pauseIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
PendingIntent stopPending =
|
||||
PendingIntent.getService(getBaseContext(), notificationID, stopIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
NotificationCompat.Action pause = new NotificationCompat.Action(R.drawable.ic_pause_black_24dp,
|
||||
getString(R.string.download_pause), pausePending);
|
||||
NotificationCompat.Action stop = new NotificationCompat.Action(R.drawable.ic_stop_black_24dp,
|
||||
getString(R.string.download_stop), stopPending);
|
||||
|
||||
if (flags == START_FLAG_REDELIVERY && book.file == null) {
|
||||
return START_NOT_STICKY;
|
||||
} else {
|
||||
notification.put(notificationID,
|
||||
new NotificationCompat.Builder(this, ONGOING_DOWNLOAD_CHANNEL_ID)
|
||||
.setContentTitle(
|
||||
getResources().getString(R.string.zim_file_downloading) + " " + notificationTitle)
|
||||
.setProgress(100, 0, false)
|
||||
.setSmallIcon(R.drawable.kiwix_notification)
|
||||
.setColor(Color.BLACK)
|
||||
.setContentIntent(pendingIntent)
|
||||
.addAction(pause)
|
||||
.addAction(stop)
|
||||
.setOngoing(true));
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(NOTIFICATION_TITLE_KEY, notificationTitle);
|
||||
notification.get(notificationID).addExtras(bundle);
|
||||
notificationManager.notify(notificationID, notification.get(notificationID).build());
|
||||
downloadStatus.put(notificationID, PLAY);
|
||||
//LibraryFragment.downloadingBooks.remove(book);
|
||||
String url = intent.getExtras().getString(DownloadIntent.DOWNLOAD_URL_PARAMETER);
|
||||
downloadBook(url, notificationID, book);
|
||||
}
|
||||
return START_REDELIVER_INTENT;
|
||||
}
|
||||
|
||||
public void stopDownload(int notificationID) {
|
||||
Log.i(KIWIX_TAG, "Stopping ZIM Download for notificationID: " + notificationID);
|
||||
downloadStatus.put(notificationID, CANCEL);
|
||||
synchronized (pauseLock) {
|
||||
pauseLock.notify();
|
||||
}
|
||||
//if (!DownloadFragment.downloads.isEmpty()) {
|
||||
// DownloadFragment.downloads.remove(notificationID);
|
||||
// DownloadFragment.downloadFiles.remove(notificationID);
|
||||
// DownloadFragment.downloadAdapter.notifyDataSetChanged();
|
||||
//}
|
||||
updateForeground();
|
||||
notificationManager.cancel(notificationID);
|
||||
}
|
||||
|
||||
public void cancelNotification(int notificationID) {
|
||||
if (notificationManager != null) {
|
||||
notificationManager.cancel(notificationID);
|
||||
}
|
||||
}
|
||||
|
||||
public String checkWritable(String path) {
|
||||
try {
|
||||
File f = new File(path);
|
||||
f.mkdir();
|
||||
if (f.canWrite()) {
|
||||
return path;
|
||||
}
|
||||
Toast.makeText(this, getResources().getString(R.string.path_not_writable), Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return Environment.getExternalStorageDirectory().getPath();
|
||||
} catch (Exception e) {
|
||||
Toast.makeText(this, getResources().getString(R.string.path_not_writable), Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return Environment.getExternalStorageDirectory().getPath();
|
||||
}
|
||||
}
|
||||
|
||||
public void toggleDownload(int notificationID) {
|
||||
if (downloadStatus.get(notificationID) == PAUSE) {
|
||||
playDownload(notificationID);
|
||||
} else {
|
||||
pauseDownload(notificationID);
|
||||
}
|
||||
}
|
||||
|
||||
public void pauseDownload(int notificationID) {
|
||||
Log.i(KIWIX_TAG, "Pausing ZIM Download for notificationID: " + notificationID);
|
||||
downloadStatus.put(notificationID, PAUSE);
|
||||
//notification.get(notificationID).mActions.get(0).title = getString(R.string.download_resume);
|
||||
//notification.get(notificationID).mActions.get(0).icon = R.drawable.ic_play_arrow_black_24dp;
|
||||
notification.get(notificationID).setContentText(getString(R.string.download_paused));
|
||||
notificationManager.notify(notificationID, notification.get(notificationID).build());
|
||||
// if (DownloadFragment.downloadAdapter != null) {
|
||||
// DownloadFragment.downloadAdapter.notifyDataSetChanged();
|
||||
// downloadFragment.listView.invalidateViews();
|
||||
// }
|
||||
}
|
||||
|
||||
public boolean playDownload(int notificationID) {
|
||||
Log.i(KIWIX_TAG, "Starting ZIM Download for notificationID: " + notificationID);
|
||||
downloadStatus.put(notificationID, PLAY);
|
||||
synchronized (pauseLock) {
|
||||
pauseLock.notify();
|
||||
}
|
||||
// notification.get(notificationID).mActions.get(0).title = getString(R.string.download_pause);
|
||||
// notification.get(notificationID).mActions.get(0).icon = R.drawable.ic_pause_black_24dp;
|
||||
notification.get(notificationID).setContentText("");
|
||||
notificationManager.notify(notificationID, notification.get(notificationID).build());
|
||||
// if (DownloadFragment.downloadAdapter != null) {
|
||||
// DownloadFragment.downloadAdapter.notifyDataSetChanged();
|
||||
// downloadFragment.listView.invalidateViews();
|
||||
// }
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void downloadBook(String url, int notificationID, LibraryNetworkEntity.Book book) {
|
||||
//if (downloadFragment != null) {
|
||||
// downloadFragment.addDownload(notificationID, book,
|
||||
// KIWIX_ROOT + StorageUtils.getFileNameFromUrl(book.getUrl()));
|
||||
//}
|
||||
TestingUtils.bindResource(DownloadService.class);
|
||||
if (book.file != null && (book.file.exists() || new File(
|
||||
book.file.getPath() + ".part").exists())) {
|
||||
// Calculate initial download progress
|
||||
int initial =
|
||||
(int) (FileUtils.getCurrentSize(book) / (Long.valueOf(book.getSize()) * BOOK_SIZE_OFFSET));
|
||||
notification.get(notificationID).setProgress(100, initial, false);
|
||||
updateDownloadFragmentProgress(initial, notificationID);
|
||||
notificationManager.notify(notificationID, notification.get(notificationID).build());
|
||||
}
|
||||
kiwixService.getMetaLinks(url)
|
||||
.retryWhen(errors -> errors.flatMap(error -> Observable.timer(5, TimeUnit.SECONDS)))
|
||||
.subscribeOn(AndroidSchedulers.mainThread())
|
||||
.flatMap(metaLink -> getMetaLinkContentLength(metaLink.getRelevantUrl().getValue()))
|
||||
.flatMap(pair -> Observable.fromIterable(
|
||||
ChunkUtils.getChunks(pair.first, pair.second, notificationID)))
|
||||
.concatMap(this::downloadChunk)
|
||||
.distinctUntilChanged().doOnComplete(() -> updateDownloadFragmentComplete(notificationID))
|
||||
.subscribe(new Observer<Integer>() {
|
||||
@Override
|
||||
public void onSubscribe(Disposable d) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(Integer progress) {
|
||||
if (progress == 100) {
|
||||
notification.get(notificationID).setOngoing(false);
|
||||
Bundle b = notification.get(notificationID).getExtras();
|
||||
notification.get(notificationID)
|
||||
.setContentTitle(
|
||||
b.getString(NOTIFICATION_TITLE_KEY) + " " + getResources().getString(
|
||||
R.string.zim_file_downloaded));
|
||||
notification.get(notificationID).getExtras();
|
||||
notification.get(notificationID)
|
||||
.setContentText(getString(R.string.zim_file_downloaded));
|
||||
final Intent target = new Intent(DownloadService.this, MainActivity.class);
|
||||
target.putExtra(EXTRA_ZIM_FILE,
|
||||
KIWIX_ROOT + StorageUtils.getFileNameFromUrl(book.getUrl()));
|
||||
//Remove the extra ".part" from files
|
||||
String filename = book.file.getPath();
|
||||
if (filename.endsWith(ZIM_EXTENSION)) {
|
||||
filename = filename + PART;
|
||||
File partFile = new File(filename);
|
||||
if (partFile.exists()) {
|
||||
partFile.renameTo(new File(partFile.getPath().replaceAll(".part", "")));
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; true; i++) {
|
||||
char first = ALPHABET.charAt(i / 26);
|
||||
char second = ALPHABET.charAt(i % 26);
|
||||
String chunkExtension = String.valueOf(first) + second;
|
||||
filename = book.file.getPath();
|
||||
filename = filename.replaceAll(".zim([a-z][a-z]){0,1}$", ".zim");
|
||||
filename = filename + chunkExtension + ".part";
|
||||
File partFile = new File(filename);
|
||||
if (partFile.exists()) {
|
||||
partFile.renameTo(new File(partFile.getPath().replaceAll(".part$", "")));
|
||||
} else {
|
||||
File lastChunkFile = new File(filename + ".part");
|
||||
if (lastChunkFile.exists()) {
|
||||
lastChunkFile.renameTo(new File(partFile.getPath().replaceAll(".part", "")));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
target.putExtra(EXTRA_NOTIFICATION_ID, notificationID);
|
||||
target.setAction(Long.toString(System.currentTimeMillis()));
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity
|
||||
(getBaseContext(), 0,
|
||||
target, PendingIntent.FLAG_ONE_SHOT);
|
||||
//book.downloaded = true;
|
||||
//dataSource.deleteBook(book)
|
||||
// .subscribe(new CompletableObserver() {
|
||||
// @Override
|
||||
// public void onSubscribe(Disposable d) {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onComplete() {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onError(Throwable e) {
|
||||
// Log.e("DownloadService", "Unable to delete book", e);
|
||||
// }
|
||||
// });
|
||||
notification.get(notificationID).setContentIntent(pendingIntent);
|
||||
//notification.get(notificationID).mActions.clear();
|
||||
TestingUtils.unbindResource(DownloadService.class);
|
||||
}
|
||||
notification.get(notificationID).setProgress(100, progress, false);
|
||||
if (progress != 100 && timeRemaining.get(notificationID) != -1) {
|
||||
//notification.get(notificationID)
|
||||
// .setContentText(
|
||||
// DownloadFragment.toHumanReadableTime(timeRemaining.get(notificationID)));
|
||||
}
|
||||
notificationManager.notify(notificationID, notification.get(notificationID).build());
|
||||
if (progress == 0 || progress == 100) {
|
||||
// Tells android to not kill the service
|
||||
updateForeground();
|
||||
}
|
||||
updateDownloadFragmentProgress(progress, notificationID);
|
||||
if (progress == 100) {
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable e) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateDownloadFragmentProgress(int progress, int notificationID) {
|
||||
//if (DownloadFragment.downloads != null
|
||||
// && DownloadFragment.downloads.get(notificationID) != null) {
|
||||
// handler.post(() -> {
|
||||
// if (DownloadFragment.downloads.get(notificationID) != null) {
|
||||
// DownloadFragment.downloadAdapter.updateProgress(progress, notificationID);
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
}
|
||||
|
||||
private void updateDownloadFragmentComplete(int notificationID) {
|
||||
//if (DownloadFragment.downloads != null
|
||||
// && DownloadFragment.downloads.get(notificationID) != null) {
|
||||
// handler.post(() -> {
|
||||
// if (DownloadFragment.downloads.get(notificationID) != null) {
|
||||
// DownloadFragment.downloadAdapter.complete(notificationID);
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
}
|
||||
|
||||
private void updateForeground() {
|
||||
// Allow notification to be dismissible while ensuring integrity of service if active downloads
|
||||
stopForeground(true);
|
||||
for (int i = 0; i < downloadStatus.size(); i++) {
|
||||
if (downloadStatus.get(i) == PLAY && downloadStatus.get(i) == PAUSE) {
|
||||
startForeground(downloadStatus.keyAt(i), notification.get(downloadStatus.keyAt(i)).build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Observable<Pair<String, Long>> getMetaLinkContentLength(String url) {
|
||||
Log.d("KiwixDownloadSSL", "url=" + url);
|
||||
final String urlToUse = UseHttpOnAndroidVersion4(url);
|
||||
return Observable.create(subscriber -> {
|
||||
try {
|
||||
Request request = new Request.Builder().url(urlToUse).head().build();
|
||||
Response response = httpClient.newCall(request).execute();
|
||||
String LengthHeader = response.headers().get("Content-Length");
|
||||
long contentLength = LengthHeader == null ? 0 : Long.parseLong(LengthHeader);
|
||||
subscriber.onNext(new Pair<>(urlToUse, contentLength));
|
||||
subscriber.onComplete();
|
||||
if (!response.isSuccessful()) subscriber.onError(new Exception(response.message()));
|
||||
} catch (IOException e) {
|
||||
subscriber.onError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String UseHttpOnAndroidVersion4(String sourceUrl) {
|
||||
|
||||
// Simply return the current URL on newer builds of Android
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
return sourceUrl;
|
||||
}
|
||||
|
||||
// Otherwise replace https with http to bypass Android 4.x devices having older certificates
|
||||
// See https://github.com/kiwix/kiwix-android/issues/510 for details
|
||||
try {
|
||||
URL tempURL = new URL(sourceUrl);
|
||||
String androidV4URL = "http" + sourceUrl.substring(tempURL.getProtocol().length());
|
||||
Log.d("KiwixDownloadSSL", "replacement_url=" + androidV4URL);
|
||||
return androidV4URL;
|
||||
} catch (MalformedURLException e) {
|
||||
return sourceUrl;
|
||||
}
|
||||
}
|
||||
|
||||
private Observable<Integer> downloadChunk(Chunk chunk) {
|
||||
return Observable.create(subscriber -> {
|
||||
try {
|
||||
// Stop if download is completed or download canceled
|
||||
if (chunk.isDownloaded || downloadStatus.get(chunk.getNotificationID()) == CANCEL) {
|
||||
subscriber.onComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
// Create chunk file
|
||||
File file = new File(KIWIX_ROOT, chunk.getFileName());
|
||||
file.getParentFile().mkdirs();
|
||||
File fullFile =
|
||||
new File(file.getPath().substring(0, file.getPath().length() - PART.length()));
|
||||
|
||||
long downloaded = Long.parseLong(chunk.getRangeHeader().split("-")[0]);
|
||||
if (fullFile.exists() && fullFile.length() == chunk.getSize()) {
|
||||
// Mark chunk status as downloaded
|
||||
chunk.isDownloaded = true;
|
||||
subscriber.onComplete();
|
||||
return;
|
||||
} else if (!file.exists()) {
|
||||
file.createNewFile();
|
||||
}
|
||||
|
||||
RandomAccessFile output = new RandomAccessFile(file, "rw");
|
||||
output.seek(output.length());
|
||||
downloaded += output.length();
|
||||
|
||||
if (chunk.getStartByte() == 0) {
|
||||
//if (!DownloadFragment.downloads.isEmpty()) {
|
||||
// LibraryNetworkEntity.Book book = DownloadFragment.downloads
|
||||
// .get(chunk.getNotificationID());
|
||||
// book.remoteUrl = book.getUrl();
|
||||
// book.file = fullFile;
|
||||
// dataSource.saveBook(book)
|
||||
// .subscribe(new CompletableObserver() {
|
||||
// @Override
|
||||
// public void onSubscribe(Disposable d) {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onComplete() {
|
||||
//
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void onError(Throwable e) {
|
||||
// Log.e("DownloadService", "Unable to save book", e);
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
downloadStatus.put(chunk.getNotificationID(), PLAY);
|
||||
downloadProgress.put(chunk.getNotificationID(), 0);
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[2048];
|
||||
int read;
|
||||
int timeout = 100;
|
||||
int attempts = 0;
|
||||
BufferedSource input = null;
|
||||
|
||||
// Keep attempting to download chunk despite network errors
|
||||
while (attempts < timeout) {
|
||||
try {
|
||||
String rangeHeader = String.format(Locale.US, "%d-%d", downloaded, chunk.getEndByte());
|
||||
|
||||
// Build request with up to date range
|
||||
Response response = httpClient.newCall(
|
||||
new Request.Builder()
|
||||
.url(chunk.getUrl())
|
||||
.header("Range", "bytes=" + rangeHeader)
|
||||
.build()
|
||||
).execute();
|
||||
|
||||
// Check that the server is sending us the right file
|
||||
if (Math.abs(chunk.getEndByte() - downloaded - response.body().contentLength()) > 10) {
|
||||
throw new Exception("Server broadcasting wrong size");
|
||||
}
|
||||
|
||||
input = response.body().source();
|
||||
|
||||
Log.d("kiwixdownloadservice", "Got valid chunk");
|
||||
|
||||
long lastTime = System.currentTimeMillis();
|
||||
long lastSize = 0;
|
||||
|
||||
// Start streaming data
|
||||
while ((read = input.read(buffer)) != -1) {
|
||||
if (downloadStatus.get(chunk.getNotificationID()) == CANCEL) {
|
||||
attempts = timeout;
|
||||
break;
|
||||
}
|
||||
|
||||
if (MainActivity.wifiOnly && !NetworkUtils.isWiFi(getApplicationContext()) ||
|
||||
!NetworkUtils.isNetworkAvailable(getApplicationContext())) {
|
||||
pauseDownload(chunk.getNotificationID());
|
||||
}
|
||||
|
||||
if (downloadStatus.get(chunk.getNotificationID()) == PAUSE) {
|
||||
synchronized (pauseLock) {
|
||||
try {
|
||||
timeRemaining.put(chunk.getNotificationID(), -1);
|
||||
// Calling wait() will block this thread until another thread
|
||||
// calls notify() on the object.
|
||||
pauseLock.wait();
|
||||
|
||||
lastTime = System.currentTimeMillis();
|
||||
lastSize = downloaded;
|
||||
} catch (InterruptedException e) {
|
||||
// Happens if someone interrupts your thread.
|
||||
}
|
||||
}
|
||||
}
|
||||
downloaded += read;
|
||||
|
||||
long timeDiff = System.currentTimeMillis() - lastTime;
|
||||
if (timeDiff >= 1000) {
|
||||
lastTime = System.currentTimeMillis();
|
||||
double speed = (downloaded - lastSize) / (timeDiff / 1000.0);
|
||||
lastSize = downloaded;
|
||||
int secondsLeft = (int) ((chunk.getContentLength() - downloaded) / speed);
|
||||
|
||||
timeRemaining.put(chunk.getNotificationID(), secondsLeft);
|
||||
}
|
||||
|
||||
output.write(buffer, 0, read);
|
||||
int progress = (int) ((100 * downloaded) / chunk.getContentLength());
|
||||
downloadProgress.put(chunk.getNotificationID(), progress);
|
||||
if (progress == 100) {
|
||||
downloadStatus.put(chunk.getNotificationID(), FINISH);
|
||||
}
|
||||
subscriber.onNext(progress);
|
||||
}
|
||||
attempts = timeout;
|
||||
} catch (Exception e) {
|
||||
// Retry on network error
|
||||
attempts++;
|
||||
Log.d(KIWIX_TAG, "Download Attempt Failed [" + attempts + "] times", e);
|
||||
try {
|
||||
Thread.sleep(1000 * attempts); // The more unsuccessful attempts the longer the wait
|
||||
} catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (input != null) {
|
||||
input.close();
|
||||
}
|
||||
// If download is canceled clean up else remove .part from file name
|
||||
if (downloadStatus.get(chunk.getNotificationID()) == CANCEL) {
|
||||
String path = file.getPath();
|
||||
Log.i(KIWIX_TAG, "Download Cancelled, deleting file: " + path);
|
||||
if (path.substring(path.length() - (ZIM_EXTENSION + PART).length())
|
||||
.equals(ZIM_EXTENSION + PART)) {
|
||||
path = path.substring(0, path.length() - PART.length() + 1);
|
||||
FileUtils.deleteZimFile(path);
|
||||
} else {
|
||||
path = path.substring(0, path.length() - (ZIM_EXTENSION + PART).length() + 2) + "aa";
|
||||
FileUtils.deleteZimFile(path);
|
||||
}
|
||||
} else {
|
||||
Log.i(KIWIX_TAG,
|
||||
"Download completed, renaming file ([" + file.getPath() + "] -> .zim.part)");
|
||||
file.renameTo(new File(file.getPath().replaceAll(".part$", "")));
|
||||
}
|
||||
// Mark chunk status as downloaded
|
||||
chunk.isDownloaded = true;
|
||||
subscriber.onComplete();
|
||||
} catch (IOException e) {
|
||||
// Catch unforeseen file system errors
|
||||
subscriber.onError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and registers notification channel with system for notifications of
|
||||
* type: download in progress.
|
||||
*/
|
||||
private void createOngoingDownloadChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = getString(R.string.ongoing_download_channel_name);
|
||||
String description = getString(R.string.ongoing_download_channel_desc);
|
||||
int importance = NotificationManager.IMPORTANCE_DEFAULT;
|
||||
NotificationChannel ongoingDownloadsChannel = new NotificationChannel(
|
||||
Constants.ONGOING_DOWNLOAD_CHANNEL_ID, name, importance);
|
||||
ongoingDownloadsChannel.setDescription(description);
|
||||
ongoingDownloadsChannel.setSound(null, null);
|
||||
NotificationManager notificationManager = (NotificationManager) getSystemService(
|
||||
NOTIFICATION_SERVICE);
|
||||
notificationManager.createNotificationChannel(ongoingDownloadsChannel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class used for the client Binder. Because we know this service always
|
||||
* runs in the same process as its clients, we don't need to deal with IPC.
|
||||
*/
|
||||
public class LocalBinder extends Binder {
|
||||
public DownloadService getService() {
|
||||
// Return this instance of LocalService so clients can call public methods
|
||||
return DownloadService.this;
|
||||
}
|
||||
}
|
||||
}
|
@ -25,6 +25,7 @@ import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.download_item.description
|
||||
import kotlinx.android.synthetic.main.download_item.downloadProgress
|
||||
import kotlinx.android.synthetic.main.download_item.downloadState
|
||||
import kotlinx.android.synthetic.main.download_item.eta
|
||||
import kotlinx.android.synthetic.main.download_item.favicon
|
||||
import kotlinx.android.synthetic.main.download_item.stop
|
||||
import kotlinx.android.synthetic.main.download_item.title
|
||||
@ -35,7 +36,6 @@ import org.kiwix.kiwixmobile.downloader.model.DownloadState.Paused
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadState.Pending
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadState.Running
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadState.Successful
|
||||
import org.kiwix.kiwixmobile.downloader.model.FailureReason.Rfc2616HttpCode
|
||||
import org.kiwix.kiwixmobile.extensions.setBitmap
|
||||
|
||||
class DownloadViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView),
|
||||
@ -51,36 +51,21 @@ class DownloadViewHolder(override val containerView: View) : RecyclerView.ViewHo
|
||||
stop.setOnClickListener {
|
||||
itemClickListener.invoke(downloadItem)
|
||||
}
|
||||
downloadState.text = toReadableState(
|
||||
downloadItem.downloadState, containerView.context
|
||||
)
|
||||
downloadState.text = toReadableState(downloadItem.downloadState, containerView.context)
|
||||
eta.text = downloadItem.eta.takeIf { it.seconds > 0L }?.toHumanReadableTime() ?: ""
|
||||
}
|
||||
|
||||
private fun toReadableState(
|
||||
downloadState: DownloadState,
|
||||
context: Context
|
||||
) = when (downloadState) {
|
||||
is Paused -> context.getString(
|
||||
downloadState.stringId,
|
||||
context.getString(downloadState.reason.stringId)
|
||||
)
|
||||
is Failed -> context.getString(
|
||||
downloadState.stringId,
|
||||
getTemplateString(downloadState, context)
|
||||
downloadState.reason.name
|
||||
)
|
||||
Pending,
|
||||
Running,
|
||||
Paused,
|
||||
Successful -> context.getString(downloadState.stringId)
|
||||
}
|
||||
|
||||
private fun getTemplateString(
|
||||
downloadState: Failed,
|
||||
context: Context
|
||||
) = when (downloadState.reason) {
|
||||
is Rfc2616HttpCode -> context.getString(
|
||||
downloadState.reason.stringId,
|
||||
downloadState.reason.code
|
||||
)
|
||||
else -> context.getString(downloadState.reason.stringId)
|
||||
}
|
||||
}
|
||||
|
@ -18,12 +18,9 @@
|
||||
package org.kiwix.kiwixmobile.downloader
|
||||
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadItem
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadStatus
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity
|
||||
|
||||
interface Downloader {
|
||||
fun download(book: LibraryNetworkEntity.Book)
|
||||
fun queryStatus(downloadModels: List<DownloadModel>): List<DownloadStatus>
|
||||
fun cancelDownload(downloadItem: DownloadItem)
|
||||
}
|
||||
|
@ -19,17 +19,15 @@
|
||||
package org.kiwix.kiwixmobile.downloader
|
||||
|
||||
import org.kiwix.kiwixmobile.data.remote.KiwixService
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewDownloadDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.FetchDownloadDao
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadItem
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadRequest
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadStatus
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity
|
||||
import javax.inject.Inject
|
||||
|
||||
class DownloaderImpl @Inject constructor(
|
||||
private val downloadRequester: DownloadRequester,
|
||||
private val downloadDao: NewDownloadDao,
|
||||
private val downloadDao: FetchDownloadDao,
|
||||
private val kiwixService: KiwixService
|
||||
) : Downloader {
|
||||
|
||||
@ -42,21 +40,14 @@ class DownloaderImpl @Inject constructor(
|
||||
val downloadId = downloadRequester.enqueue(
|
||||
DownloadRequest(it, book)
|
||||
)
|
||||
downloadDao.insert(
|
||||
DownloadModel(downloadId = downloadId, book = book)
|
||||
)
|
||||
downloadDao.insert(downloadId, book = book)
|
||||
}
|
||||
},
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
}
|
||||
|
||||
override fun queryStatus(downloadModels: List<DownloadModel>) =
|
||||
downloadRequester.query(downloadModels)
|
||||
.sortedBy(DownloadStatus::downloadId)
|
||||
|
||||
override fun cancelDownload(downloadItem: DownloadItem) {
|
||||
downloadRequester.cancel(downloadItem)
|
||||
downloadDao.delete(downloadItem.downloadId)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (C) 2018 Kiwix <android.kiwix.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.downloader.fetch
|
||||
|
||||
import com.tonyodev.fetch2.Download
|
||||
import com.tonyodev.fetch2.Error
|
||||
import com.tonyodev.fetch2.Fetch
|
||||
import com.tonyodev.fetch2.FetchListener
|
||||
import com.tonyodev.fetch2core.DownloadBlock
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.FetchDownloadDao
|
||||
import org.kiwix.kiwixmobile.downloader.DownloadMonitor
|
||||
import javax.inject.Inject
|
||||
|
||||
class FetchDownloadMonitor @Inject constructor(fetch: Fetch, fetchDownloadDao: FetchDownloadDao) :
|
||||
DownloadMonitor {
|
||||
private val updater = PublishSubject.create<() -> Unit>()
|
||||
private val fetchListener = object : FetchListener {
|
||||
override fun onAdded(download: Download) {}
|
||||
|
||||
override fun onCancelled(download: Download) {
|
||||
delete(download)
|
||||
}
|
||||
|
||||
override fun onCompleted(download: Download) {
|
||||
update(download)
|
||||
}
|
||||
|
||||
override fun onDeleted(download: Download) {
|
||||
delete(download)
|
||||
}
|
||||
|
||||
override fun onDownloadBlockUpdated(
|
||||
download: Download,
|
||||
downloadBlock: DownloadBlock,
|
||||
totalBlocks: Int
|
||||
) {
|
||||
update(download)
|
||||
}
|
||||
|
||||
override fun onError(download: Download, error: Error, throwable: Throwable?) {
|
||||
update(download)
|
||||
}
|
||||
|
||||
override fun onPaused(download: Download) {
|
||||
update(download)
|
||||
}
|
||||
|
||||
override fun onProgress(
|
||||
download: Download,
|
||||
etaInMilliSeconds: Long,
|
||||
downloadedBytesPerSecond: Long
|
||||
) {
|
||||
update(download)
|
||||
}
|
||||
|
||||
override fun onQueued(download: Download, waitingOnNetwork: Boolean) {
|
||||
update(download)
|
||||
}
|
||||
|
||||
override fun onRemoved(download: Download) {
|
||||
delete(download)
|
||||
}
|
||||
|
||||
override fun onResumed(download: Download) {
|
||||
update(download)
|
||||
}
|
||||
|
||||
override fun onStarted(
|
||||
download: Download,
|
||||
downloadBlocks: List<DownloadBlock>,
|
||||
totalBlocks: Int
|
||||
) {
|
||||
update(download)
|
||||
}
|
||||
|
||||
override fun onWaitingNetwork(download: Download) {
|
||||
update(download)
|
||||
}
|
||||
|
||||
private fun update(download: Download) {
|
||||
updater.onNext { fetchDownloadDao.update(download) }
|
||||
}
|
||||
|
||||
private fun delete(download: Download) {
|
||||
updater.onNext { fetchDownloadDao.delete(download) }
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
fetch.addListener(fetchListener, true)
|
||||
updater.subscribeOn(Schedulers.io()).observeOn(Schedulers.io()).subscribe(
|
||||
{ it.invoke() },
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
}
|
||||
|
||||
override fun init() {
|
||||
// empty method to so class does not get reported unused
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (C) 2018 Kiwix <android.kiwix.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.downloader.fetch
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Build.VERSION_CODES
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.tonyodev.fetch2.DefaultFetchNotificationManager
|
||||
import com.tonyodev.fetch2.DownloadNotification
|
||||
import com.tonyodev.fetch2.Fetch
|
||||
import com.tonyodev.fetch2.util.DEFAULT_NOTIFICATION_TIMEOUT_AFTER_RESET
|
||||
import org.kiwix.kiwixmobile.R
|
||||
import org.kiwix.kiwixmobile.R.string
|
||||
|
||||
class FetchDownloadNotificationManager(context: Context) :
|
||||
DefaultFetchNotificationManager(context) {
|
||||
override fun getFetchInstanceForNamespace(namespace: String) = Fetch.getDefaultInstance()
|
||||
|
||||
override fun createNotificationChannels(
|
||||
context: Context,
|
||||
notificationManager: NotificationManager
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channelId = context.getString(R.string.fetch_notification_default_channel_id)
|
||||
if (notificationManager.getNotificationChannel(channelId) == null) {
|
||||
notificationManager.createNotificationChannel(createChannel(channelId, context))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateNotification(
|
||||
notificationBuilder: NotificationCompat.Builder,
|
||||
downloadNotification: DownloadNotification,
|
||||
context: Context
|
||||
) {
|
||||
val smallIcon = if (downloadNotification.isDownloading) {
|
||||
android.R.drawable.stat_sys_download
|
||||
} else {
|
||||
android.R.drawable.stat_sys_download_done
|
||||
}
|
||||
notificationBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setSmallIcon(smallIcon)
|
||||
.setContentTitle(downloadNotification.title)
|
||||
.setContentText(getSubtitleText(context, downloadNotification))
|
||||
.setOngoing(downloadNotification.isOnGoingNotification)
|
||||
.setGroup(downloadNotification.groupId.toString())
|
||||
.setSound(null)
|
||||
.setSound(null, AudioManager.STREAM_NOTIFICATION)
|
||||
.setVibrate(null)
|
||||
.setGroupSummary(false)
|
||||
if (downloadNotification.isFailed || downloadNotification.isCompleted) {
|
||||
notificationBuilder.setProgress(0, 0, false)
|
||||
} else {
|
||||
val progressIndeterminate = downloadNotification.progressIndeterminate
|
||||
val maxProgress = if (downloadNotification.progressIndeterminate) 0 else 100
|
||||
val progress = if (downloadNotification.progress < 0) 0 else downloadNotification.progress
|
||||
notificationBuilder.setProgress(maxProgress, progress, progressIndeterminate)
|
||||
}
|
||||
when {
|
||||
downloadNotification.isDownloading ||
|
||||
downloadNotification.isPaused ||
|
||||
downloadNotification.isQueued -> {
|
||||
notificationBuilder.setTimeoutAfter(getNotificationTimeOutMillis())
|
||||
}
|
||||
else -> {
|
||||
notificationBuilder.setTimeoutAfter(DEFAULT_NOTIFICATION_TIMEOUT_AFTER_RESET)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(VERSION_CODES.O)
|
||||
private fun createChannel(channelId: String, context: Context) =
|
||||
NotificationChannel(
|
||||
channelId,
|
||||
context.getString(string.fetch_notification_default_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
setSound(null, null)
|
||||
enableVibration(false)
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (C) 2018 Kiwix <android.kiwix.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.downloader.fetch
|
||||
|
||||
import com.tonyodev.fetch2.Fetch
|
||||
import com.tonyodev.fetch2.NetworkType.ALL
|
||||
import com.tonyodev.fetch2.NetworkType.WIFI_ONLY
|
||||
import com.tonyodev.fetch2.Request
|
||||
import org.kiwix.kiwixmobile.downloader.DownloadRequester
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadItem
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadRequest
|
||||
import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil
|
||||
import javax.inject.Inject
|
||||
|
||||
class FetchDownloadRequester @Inject constructor(
|
||||
private val fetch: Fetch,
|
||||
private val sharedPreferenceUtil: SharedPreferenceUtil
|
||||
) : DownloadRequester {
|
||||
|
||||
override fun enqueue(downloadRequest: DownloadRequest): Long {
|
||||
val request = downloadRequest.toFetchRequest(sharedPreferenceUtil)
|
||||
fetch.enqueue(request)
|
||||
return request.id.toLong()
|
||||
}
|
||||
|
||||
override fun cancel(downloadItem: DownloadItem) {
|
||||
fetch.delete(downloadItem.downloadId.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
private fun DownloadRequest.toFetchRequest(sharedPreferenceUtil: SharedPreferenceUtil) =
|
||||
Request("$uri", getDestination(sharedPreferenceUtil)).apply {
|
||||
networkType = if (sharedPreferenceUtil.prefWifiOnly) WIFI_ONLY else ALL
|
||||
autoRetryMaxAttempts = 10
|
||||
}
|
@ -17,6 +17,20 @@
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.downloader.model
|
||||
|
||||
import com.tonyodev.fetch2.Error
|
||||
import com.tonyodev.fetch2.Status
|
||||
import com.tonyodev.fetch2.Status.ADDED
|
||||
import com.tonyodev.fetch2.Status.CANCELLED
|
||||
import com.tonyodev.fetch2.Status.COMPLETED
|
||||
import com.tonyodev.fetch2.Status.DELETED
|
||||
import com.tonyodev.fetch2.Status.DOWNLOADING
|
||||
import com.tonyodev.fetch2.Status.FAILED
|
||||
import com.tonyodev.fetch2.Status.NONE
|
||||
import com.tonyodev.fetch2.Status.PAUSED
|
||||
import com.tonyodev.fetch2.Status.QUEUED
|
||||
import com.tonyodev.fetch2.Status.REMOVED
|
||||
import org.kiwix.kiwixmobile.R
|
||||
|
||||
data class DownloadItem(
|
||||
val downloadId: Long,
|
||||
val favIcon: Base64String,
|
||||
@ -24,17 +38,47 @@ data class DownloadItem(
|
||||
val description: String,
|
||||
val bytesDownloaded: Long,
|
||||
val totalSizeBytes: Long,
|
||||
val progress: Int,
|
||||
val eta: Seconds,
|
||||
val downloadState: DownloadState
|
||||
) {
|
||||
val progress get() = ((bytesDownloaded.toFloat() / totalSizeBytes) * 100).toInt()
|
||||
|
||||
constructor(downloadStatus: DownloadStatus) : this(
|
||||
downloadStatus.downloadId,
|
||||
Base64String(downloadStatus.book.favicon),
|
||||
downloadStatus.title,
|
||||
downloadStatus.description,
|
||||
downloadStatus.bytesDownloadedSoFar,
|
||||
downloadStatus.totalSizeBytes,
|
||||
downloadStatus.state
|
||||
constructor(downloadModel: DownloadModel) : this(
|
||||
downloadModel.downloadId,
|
||||
Base64String(downloadModel.book.favicon),
|
||||
downloadModel.book.title,
|
||||
downloadModel.book.description,
|
||||
downloadModel.bytesDownloaded,
|
||||
downloadModel.totalSizeOfDownload,
|
||||
downloadModel.progress,
|
||||
Seconds(downloadModel.etaInMilliSeconds / 1000L),
|
||||
DownloadState.from(downloadModel.state, downloadModel.error)
|
||||
)
|
||||
}
|
||||
|
||||
sealed class DownloadState(val stringId: Int) {
|
||||
|
||||
companion object {
|
||||
fun from(state: Status, error: Error): DownloadState =
|
||||
when (state) {
|
||||
NONE,
|
||||
ADDED,
|
||||
QUEUED -> Pending
|
||||
DOWNLOADING -> Running
|
||||
PAUSED -> Paused
|
||||
COMPLETED -> Successful
|
||||
CANCELLED,
|
||||
FAILED,
|
||||
REMOVED,
|
||||
DELETED -> Failed(error)
|
||||
}
|
||||
}
|
||||
|
||||
object Pending : DownloadState(R.string.pending_state)
|
||||
object Running : DownloadState(R.string.running_state)
|
||||
object Successful : DownloadState(R.string.successful_state)
|
||||
object Paused : DownloadState(R.string.paused_state)
|
||||
data class Failed(val reason: Error) : DownloadState(R.string.failed_state)
|
||||
|
||||
override fun toString(): String = javaClass.simpleName
|
||||
}
|
||||
|
@ -17,13 +17,36 @@
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.downloader.model
|
||||
|
||||
import com.tonyodev.fetch2.Error
|
||||
import com.tonyodev.fetch2.Status
|
||||
import org.kiwix.kiwixmobile.database.newdb.entities.FetchDownloadEntity
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
|
||||
import org.kiwix.kiwixmobile.utils.StorageUtils
|
||||
|
||||
data class DownloadModel(
|
||||
val databaseId: Long? = null,
|
||||
val databaseId: Long,
|
||||
val downloadId: Long,
|
||||
val file: String?,
|
||||
val etaInMilliSeconds: Long,
|
||||
val bytesDownloaded: Long,
|
||||
val totalSizeOfDownload: Long,
|
||||
val state: Status,
|
||||
val error: Error,
|
||||
val progress: Int,
|
||||
val book: Book
|
||||
) {
|
||||
val fileNameFromUrl: String get() = StorageUtils.getFileNameFromUrl(book.url)
|
||||
val fileNameFromUrl: String by lazy { StorageUtils.getFileNameFromUrl(book.url) }
|
||||
|
||||
constructor(downloadEntity: FetchDownloadEntity) : this(
|
||||
downloadEntity.id,
|
||||
downloadEntity.downloadId,
|
||||
downloadEntity.file,
|
||||
downloadEntity.etaInMilliSeconds,
|
||||
downloadEntity.bytesDownloaded,
|
||||
downloadEntity.totalSizeOfDownload,
|
||||
downloadEntity.status,
|
||||
downloadEntity.error,
|
||||
downloadEntity.progress,
|
||||
downloadEntity.toBook()
|
||||
)
|
||||
}
|
||||
|
@ -20,6 +20,8 @@ package org.kiwix.kiwixmobile.downloader.model
|
||||
import android.net.Uri
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity
|
||||
import org.kiwix.kiwixmobile.library.entity.MetaLinkNetworkEntity
|
||||
import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil
|
||||
import org.kiwix.kiwixmobile.utils.StorageUtils
|
||||
|
||||
data class DownloadRequest(
|
||||
val urlString: String,
|
||||
@ -37,4 +39,9 @@ data class DownloadRequest(
|
||||
book.title,
|
||||
book.description
|
||||
)
|
||||
|
||||
fun getDestination(sharedPreferenceUtil: SharedPreferenceUtil): String =
|
||||
"${sharedPreferenceUtil.prefStorage}/Kiwix/${
|
||||
StorageUtils.getFileNameFromUrl(urlString)
|
||||
}"
|
||||
}
|
||||
|
@ -1,168 +0,0 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (C) 2018 Kiwix <android.kiwix.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.downloader.model
|
||||
|
||||
import android.app.DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR
|
||||
import android.app.DownloadManager.COLUMN_DESCRIPTION
|
||||
import android.app.DownloadManager.COLUMN_ID
|
||||
import android.app.DownloadManager.COLUMN_LAST_MODIFIED_TIMESTAMP
|
||||
import android.app.DownloadManager.COLUMN_LOCAL_URI
|
||||
import android.app.DownloadManager.COLUMN_MEDIAPROVIDER_URI
|
||||
import android.app.DownloadManager.COLUMN_MEDIA_TYPE
|
||||
import android.app.DownloadManager.COLUMN_REASON
|
||||
import android.app.DownloadManager.COLUMN_STATUS
|
||||
import android.app.DownloadManager.COLUMN_TITLE
|
||||
import android.app.DownloadManager.COLUMN_TOTAL_SIZE_BYTES
|
||||
import android.app.DownloadManager.COLUMN_URI
|
||||
import android.app.DownloadManager.ERROR_CANNOT_RESUME
|
||||
import android.app.DownloadManager.ERROR_DEVICE_NOT_FOUND
|
||||
import android.app.DownloadManager.ERROR_FILE_ALREADY_EXISTS
|
||||
import android.app.DownloadManager.ERROR_FILE_ERROR
|
||||
import android.app.DownloadManager.ERROR_HTTP_DATA_ERROR
|
||||
import android.app.DownloadManager.ERROR_INSUFFICIENT_SPACE
|
||||
import android.app.DownloadManager.ERROR_TOO_MANY_REDIRECTS
|
||||
import android.app.DownloadManager.ERROR_UNHANDLED_HTTP_CODE
|
||||
import android.app.DownloadManager.ERROR_UNKNOWN
|
||||
import android.app.DownloadManager.PAUSED_QUEUED_FOR_WIFI
|
||||
import android.app.DownloadManager.PAUSED_UNKNOWN
|
||||
import android.app.DownloadManager.PAUSED_WAITING_FOR_NETWORK
|
||||
import android.app.DownloadManager.PAUSED_WAITING_TO_RETRY
|
||||
import android.app.DownloadManager.STATUS_FAILED
|
||||
import android.app.DownloadManager.STATUS_PAUSED
|
||||
import android.app.DownloadManager.STATUS_PENDING
|
||||
import android.app.DownloadManager.STATUS_RUNNING
|
||||
import android.app.DownloadManager.STATUS_SUCCESSFUL
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import org.kiwix.kiwixmobile.R
|
||||
import org.kiwix.kiwixmobile.extensions.get
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
|
||||
import java.io.File
|
||||
|
||||
class DownloadStatus(
|
||||
val downloadId: Long,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val state: DownloadState,
|
||||
val bytesDownloadedSoFar: Long,
|
||||
val totalSizeBytes: Long,
|
||||
val lastModified: String,
|
||||
private val localUri: String?,
|
||||
val mediaProviderUri: String?,
|
||||
val mediaType: String?,
|
||||
val uri: String?,
|
||||
val book: Book
|
||||
) {
|
||||
|
||||
fun toBookOnDisk(uriToFileConverter: UriToFileConverter) =
|
||||
BookOnDisk(book = book, file = uriToFileConverter.convert(localUri))
|
||||
|
||||
constructor(
|
||||
cursor: Cursor,
|
||||
downloadModel: DownloadModel
|
||||
) : this(
|
||||
cursor[COLUMN_ID],
|
||||
cursor[COLUMN_TITLE],
|
||||
cursor[COLUMN_DESCRIPTION],
|
||||
DownloadState.from(cursor[COLUMN_STATUS], cursor[COLUMN_REASON]),
|
||||
cursor[COLUMN_BYTES_DOWNLOADED_SO_FAR],
|
||||
cursor[COLUMN_TOTAL_SIZE_BYTES],
|
||||
cursor[COLUMN_LAST_MODIFIED_TIMESTAMP],
|
||||
cursor[COLUMN_LOCAL_URI],
|
||||
cursor[COLUMN_MEDIAPROVIDER_URI],
|
||||
cursor[COLUMN_MEDIA_TYPE],
|
||||
cursor[COLUMN_URI],
|
||||
downloadModel.book
|
||||
)
|
||||
}
|
||||
|
||||
interface UriToFileConverter {
|
||||
fun convert(uriString: String?) = File(Uri.parse(uriString).path)
|
||||
class Impl : UriToFileConverter
|
||||
}
|
||||
|
||||
sealed class DownloadState(val stringId: Int) {
|
||||
companion object {
|
||||
fun from(
|
||||
status: Int,
|
||||
reason: Int
|
||||
) = when (status) {
|
||||
STATUS_PAUSED -> Paused(PausedReason.from(reason))
|
||||
STATUS_FAILED -> Failed(FailureReason.from(reason))
|
||||
STATUS_PENDING -> Pending
|
||||
STATUS_RUNNING -> Running
|
||||
STATUS_SUCCESSFUL -> Successful
|
||||
else -> throw RuntimeException("invalid status $status")
|
||||
}
|
||||
}
|
||||
|
||||
data class Paused(val reason: PausedReason) : DownloadState(R.string.paused_state)
|
||||
data class Failed(val reason: FailureReason) : DownloadState(R.string.failed_state)
|
||||
object Pending : DownloadState(R.string.pending_state)
|
||||
object Running : DownloadState(R.string.running_state)
|
||||
object Successful : DownloadState(R.string.successful_state)
|
||||
|
||||
override fun toString(): String = javaClass.simpleName
|
||||
}
|
||||
|
||||
sealed class FailureReason(val stringId: Int) {
|
||||
companion object {
|
||||
fun from(reason: Int) = when (reason) {
|
||||
in 100..505 -> Rfc2616HttpCode(reason)
|
||||
ERROR_CANNOT_RESUME -> CannotResume
|
||||
ERROR_DEVICE_NOT_FOUND -> StorageNotFound
|
||||
ERROR_FILE_ALREADY_EXISTS -> FileAlreadyExists
|
||||
ERROR_FILE_ERROR -> UnknownFileError
|
||||
ERROR_HTTP_DATA_ERROR -> HttpError
|
||||
ERROR_INSUFFICIENT_SPACE -> InsufficientSpace
|
||||
ERROR_TOO_MANY_REDIRECTS -> TooManyRedirects
|
||||
ERROR_UNHANDLED_HTTP_CODE -> UnhandledHttpCode
|
||||
ERROR_UNKNOWN -> Unknown
|
||||
else -> Unknown
|
||||
}
|
||||
}
|
||||
|
||||
object CannotResume : FailureReason(R.string.failed_cannot_resume)
|
||||
object StorageNotFound : FailureReason(R.string.failed_storage_not_found)
|
||||
object FileAlreadyExists : FailureReason(R.string.failed_file_already_exists)
|
||||
object UnknownFileError : FailureReason(R.string.failed_unknown_file_error)
|
||||
object HttpError : FailureReason(R.string.failed_http_error)
|
||||
object InsufficientSpace : FailureReason(R.string.failed_insufficient_space)
|
||||
object TooManyRedirects : FailureReason(R.string.failed_too_many_redirects)
|
||||
object UnhandledHttpCode : FailureReason(R.string.failed_unhandled_http_code)
|
||||
object Unknown : FailureReason(R.string.failed_unknown)
|
||||
data class Rfc2616HttpCode(val code: Int) : FailureReason(R.string.failed_http_code)
|
||||
}
|
||||
|
||||
sealed class PausedReason(val stringId: Int) {
|
||||
companion object {
|
||||
fun from(reason: Int) = when (reason) {
|
||||
PAUSED_QUEUED_FOR_WIFI -> WaitingForWifi
|
||||
PAUSED_WAITING_FOR_NETWORK -> WaitingForConnectivity
|
||||
PAUSED_WAITING_TO_RETRY -> WaitingForRetry
|
||||
PAUSED_UNKNOWN -> Unknown
|
||||
else -> Unknown
|
||||
}
|
||||
}
|
||||
|
||||
object WaitingForWifi : PausedReason(R.string.paused_wifi)
|
||||
object WaitingForConnectivity : PausedReason(R.string.paused_connectivity)
|
||||
object WaitingForRetry : PausedReason(R.string.paused_retry)
|
||||
object Unknown : PausedReason(R.string.paused_unknown)
|
||||
}
|
@ -4,8 +4,8 @@ import org.kiwix.kiwixmobile.KiwixApplication
|
||||
import java.util.Locale
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
inline class Seconds(private val seconds: Int) {
|
||||
@Suppress("unused") fun toHumanReadableTime(): String {
|
||||
inline class Seconds(val seconds: Long) {
|
||||
fun toHumanReadableTime(): String {
|
||||
val minutes = 60.0
|
||||
val hours = 60 * minutes
|
||||
val days = 24 * hours
|
||||
|
@ -45,7 +45,7 @@ class AlertDialogShower @Inject constructor(
|
||||
}
|
||||
|
||||
private fun bodyArguments(dialog: KiwixDialog) =
|
||||
if (dialog is HasBodyFormatArgs) dialog.args
|
||||
if (dialog is HasBodyFormatArgs) dialog.args.toTypedArray()
|
||||
else emptyArray()
|
||||
|
||||
private fun dialogStyle() =
|
||||
|
@ -11,23 +11,10 @@ sealed class KiwixDialog(
|
||||
val negativeMessage: Int?
|
||||
) {
|
||||
|
||||
data class DeleteZim(override val args: Array<out Any>) : KiwixDialog(
|
||||
data class DeleteZim(override val args: List<Any>) : KiwixDialog(
|
||||
null, R.string.delete_zim_body, R.string.delete, R.string.no
|
||||
), HasBodyFormatArgs {
|
||||
constructor(bookOnDisk: BookOnDisk) : this(arrayOf(bookOnDisk.book.title))
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as DeleteZim
|
||||
|
||||
if (!args.contentEquals(other.args)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode() = args.contentHashCode()
|
||||
constructor(bookOnDisk: BookOnDisk) : this(listOf(bookOnDisk.book.title))
|
||||
}
|
||||
|
||||
object LocationPermissionRationale : KiwixDialog(
|
||||
@ -59,17 +46,14 @@ sealed class KiwixDialog(
|
||||
null
|
||||
)
|
||||
|
||||
data class ShowHotspotDetails(override val args: Array<out Any>) : KiwixDialog(
|
||||
data class ShowHotspotDetails(override val args: List<Any>) : KiwixDialog(
|
||||
R.string.hotspot_turned_on,
|
||||
R.string.hotspot_details_message,
|
||||
android.R.string.ok,
|
||||
null
|
||||
), HasBodyFormatArgs {
|
||||
constructor(wifiConfiguration: WifiConfiguration) : this(
|
||||
arrayOf(
|
||||
wifiConfiguration.SSID,
|
||||
wifiConfiguration.preSharedKey
|
||||
)
|
||||
listOf(wifiConfiguration.SSID, wifiConfiguration.preSharedKey)
|
||||
)
|
||||
}
|
||||
|
||||
@ -82,10 +66,10 @@ sealed class KiwixDialog(
|
||||
null
|
||||
)
|
||||
|
||||
data class FileTransferConfirmation(override val args: Array<out Any>) : KiwixDialog(
|
||||
data class FileTransferConfirmation(override val args: List<Any>) : KiwixDialog(
|
||||
null, R.string.transfer_to, R.string.yes, android.R.string.cancel
|
||||
), HasBodyFormatArgs {
|
||||
constructor(selectedPeerDeviceName: String) : this(arrayOf(selectedPeerDeviceName))
|
||||
constructor(selectedPeerDeviceName: String) : this(listOf(selectedPeerDeviceName))
|
||||
}
|
||||
|
||||
open class YesNoDialog(
|
||||
@ -103,5 +87,5 @@ sealed class KiwixDialog(
|
||||
}
|
||||
|
||||
interface HasBodyFormatArgs {
|
||||
val args: Array<out Any>
|
||||
val args: List<Any>
|
||||
}
|
||||
|
@ -2,13 +2,14 @@ package org.kiwix.kiwixmobile.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Environment;
|
||||
import android.preference.PreferenceManager;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.processors.PublishProcessor;
|
||||
import java.util.Calendar;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import org.kiwix.kiwixmobile.KiwixApplication;
|
||||
|
||||
/**
|
||||
* Manager for the Default Shared Preferences of the application.
|
||||
@ -85,8 +86,13 @@ public class SharedPreferenceUtil {
|
||||
}
|
||||
|
||||
public String getPrefStorage() {
|
||||
return sharedPreferences.getString(PREF_STORAGE,
|
||||
Environment.getExternalStorageDirectory().getPath());
|
||||
String storage = sharedPreferences.getString(PREF_STORAGE, null);
|
||||
if (storage == null) {
|
||||
storage =
|
||||
ContextCompat.getExternalFilesDirs(KiwixApplication.getInstance(), null)[0].getPath();
|
||||
putPrefStorage(storage);
|
||||
}
|
||||
return storage;
|
||||
}
|
||||
|
||||
private boolean getPrefNightMode() {
|
||||
|
@ -20,8 +20,6 @@ package org.kiwix.kiwixmobile.utils;
|
||||
public class StorageUtils {
|
||||
|
||||
public static String getFileNameFromUrl(String url) {
|
||||
String filename = NetworkUtils.getFileNameFromUrl(url);
|
||||
filename = filename.replace(".meta4", "");
|
||||
return filename;
|
||||
return NetworkUtils.getFileNameFromUrl(url).replace(".meta4", "");
|
||||
}
|
||||
}
|
||||
|
@ -20,9 +20,9 @@
|
||||
package org.kiwix.kiwixmobile.utils.files
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore.Files
|
||||
import android.provider.MediaStore.MediaColumns
|
||||
import eu.mhutti1.utils.storage.StorageDevice
|
||||
import eu.mhutti1.utils.storage.StorageDeviceUtils
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.functions.BiFunction
|
||||
@ -36,9 +36,9 @@ class FileSearch @Inject constructor(private val context: Context) {
|
||||
|
||||
private val zimFileExtensions = arrayOf("zim", "zimaa")
|
||||
|
||||
fun scan(defaultPath: String): Flowable<List<File>> =
|
||||
fun scan(): Flowable<List<File>> =
|
||||
Flowable.combineLatest(
|
||||
Flowable.fromCallable { scanFileSystem(defaultPath) }.subscribeOn(Schedulers.io()),
|
||||
Flowable.fromCallable(::scanFileSystem).subscribeOn(Schedulers.io()),
|
||||
Flowable.fromCallable(::scanMediaStore).subscribeOn(Schedulers.io()),
|
||||
BiFunction<List<File>, List<File>, List<File>> { filesSystemFiles, mediaStoreFiles ->
|
||||
filesSystemFiles + mediaStoreFiles
|
||||
@ -62,18 +62,15 @@ class FileSearch @Inject constructor(private val context: Context) {
|
||||
null
|
||||
)
|
||||
|
||||
private fun scanFileSystem(defaultPath: String) =
|
||||
directoryRoots(defaultPath)
|
||||
.minus(Environment.getExternalStorageDirectory().absolutePath)
|
||||
private fun scanFileSystem() =
|
||||
directoryRoots()
|
||||
.fold(mutableListOf<File>(), { acc, root ->
|
||||
acc.apply { addAll(scanDirectory(root)) }
|
||||
})
|
||||
.distinctBy { it.canonicalPath }
|
||||
|
||||
private fun directoryRoots(defaultPath: String) = listOf(
|
||||
"/mnt",
|
||||
defaultPath,
|
||||
*StorageDeviceUtils.getStorageDevices(context, false).map { it.name }.toTypedArray()
|
||||
)
|
||||
private fun directoryRoots() =
|
||||
StorageDeviceUtils.getReadableStorage(context).map(StorageDevice::name)
|
||||
|
||||
private fun scanDirectory(directory: String): List<File> = File(directory).listFiles()
|
||||
?.fold(
|
||||
|
@ -35,7 +35,7 @@ import java.util.ArrayList
|
||||
|
||||
object FileUtils {
|
||||
|
||||
val saveFilePath =
|
||||
private val saveFilePath =
|
||||
"${Environment.getExternalStorageDirectory()}${File.separator}Android" +
|
||||
"${File.separator}obb${File.separator}${BuildConfig.APPLICATION_ID}"
|
||||
|
||||
|
@ -1,51 +0,0 @@
|
||||
/*
|
||||
* Kiwix Android
|
||||
* Copyright (C) 2018 Kiwix <android.kiwix.org>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.zim_manager
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import org.kiwix.kiwixmobile.KiwixApplication
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewDownloadDao
|
||||
import javax.inject.Inject
|
||||
|
||||
class DownloadNotificationClickedReceiver : BaseBroadcastReceiver() {
|
||||
override val action: String = DownloadManager.ACTION_NOTIFICATION_CLICKED
|
||||
|
||||
@Inject lateinit var downloadDao: NewDownloadDao
|
||||
|
||||
override fun onIntentWithActionReceived(
|
||||
context: Context,
|
||||
intent: Intent
|
||||
) {
|
||||
KiwixApplication.getApplicationComponent()
|
||||
.inject(this)
|
||||
if (downloadDao.containsAny(*longArrayFrom(intent.extras))) {
|
||||
context.startActivity(
|
||||
Intent(context, ZimManageActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
putExtra(ZimManageActivity.TAB_EXTRA, 2)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun longArrayFrom(extras: Bundle?) =
|
||||
extras?.getLongArray(DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS) ?: longArrayOf()
|
||||
}
|
@ -17,59 +17,46 @@
|
||||
*/
|
||||
package org.kiwix.kiwixmobile.zim_manager
|
||||
|
||||
import android.Manifest.permission
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.FileObserver
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.functions.Function3
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.processors.BehaviorProcessor
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.kiwix.kiwixmobile.KiwixApplication
|
||||
import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil
|
||||
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.CanWrite4GbFile
|
||||
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.CannotWrite4GbFile
|
||||
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.NotEnoughSpaceFor4GbFile
|
||||
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.Unknown
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
import javax.inject.Inject
|
||||
|
||||
class Fat32Checker @Inject constructor(sharedPreferenceUtil: SharedPreferenceUtil) {
|
||||
private val _fileSystemStates: BehaviorProcessor<FileSystemState> = BehaviorProcessor.create()
|
||||
val fileSystemStates: Flowable<FileSystemState> = _fileSystemStates.distinctUntilChanged()
|
||||
val fileSystemStates: BehaviorProcessor<FileSystemState> = BehaviorProcessor.create()
|
||||
private var fileObserver: FileObserver? = null
|
||||
private val requestCheckSystemFileType = BehaviorProcessor.createDefault(Unit)
|
||||
|
||||
init {
|
||||
Flowable.combineLatest(
|
||||
sharedPreferenceUtil.prefStorages.distinctUntilChanged(),
|
||||
sharedPreferenceUtil.prefStorages
|
||||
.distinctUntilChanged()
|
||||
.doOnNext { fileSystemStates.offer(Unknown) },
|
||||
requestCheckSystemFileType,
|
||||
pollForExternalStoragePermissionGranted(),
|
||||
Function3 { storage: String, _: Unit, _: Boolean -> storage }
|
||||
BiFunction { storage: String, _: Unit -> storage }
|
||||
)
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe(
|
||||
{
|
||||
val systemState = toFileSystemState(it)
|
||||
_fileSystemStates.onNext(systemState)
|
||||
fileSystemStates.offer(systemState)
|
||||
fileObserver = if (systemState == NotEnoughSpaceFor4GbFile) fileObserver(it) else null
|
||||
},
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
}
|
||||
|
||||
private fun pollForExternalStoragePermissionGranted() =
|
||||
Flowable.interval(1, SECONDS)
|
||||
.map {
|
||||
ContextCompat.checkSelfPermission(
|
||||
KiwixApplication.getInstance(), permission.WRITE_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
.filter { it }
|
||||
.take(1)
|
||||
|
||||
private fun fileObserver(it: String?): FileObserver {
|
||||
return object : FileObserver(it, MOVED_FROM or DELETE) {
|
||||
override fun onEvent(
|
||||
@ -91,7 +78,7 @@ class Fat32Checker @Inject constructor(sharedPreferenceUtil: SharedPreferenceUti
|
||||
|
||||
private fun canCreate4GbFile(storage: String): Boolean {
|
||||
val path = "$storage/large_file_test.txt"
|
||||
File(path).delete()
|
||||
File(path).deleteIfExists()
|
||||
try {
|
||||
RandomAccessFile(path, "rw").use {
|
||||
it.setLength(FOUR_GIGABYTES_IN_BYTES)
|
||||
@ -102,7 +89,7 @@ class Fat32Checker @Inject constructor(sharedPreferenceUtil: SharedPreferenceUti
|
||||
Log.d("Fat32Checker", e.message)
|
||||
return false
|
||||
} finally {
|
||||
File(path).delete()
|
||||
File(path).deleteIfExists()
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,5 +102,10 @@ class Fat32Checker @Inject constructor(sharedPreferenceUtil: SharedPreferenceUti
|
||||
object NotEnoughSpaceFor4GbFile : FileSystemState()
|
||||
object CanWrite4GbFile : FileSystemState()
|
||||
object CannotWrite4GbFile : FileSystemState()
|
||||
object Unknown : FileSystemState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun File.deleteIfExists() {
|
||||
if (exists()) delete()
|
||||
}
|
||||
|
@ -33,15 +33,11 @@ import io.reactivex.schedulers.Schedulers
|
||||
import org.kiwix.kiwixmobile.R
|
||||
import org.kiwix.kiwixmobile.data.DataSource
|
||||
import org.kiwix.kiwixmobile.data.remote.KiwixService
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.FetchDownloadDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewBookDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewDownloadDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewLanguagesDao
|
||||
import org.kiwix.kiwixmobile.downloader.Downloader
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadItem
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadState.Successful
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadStatus
|
||||
import org.kiwix.kiwixmobile.downloader.model.UriToFileConverter
|
||||
import org.kiwix.kiwixmobile.extensions.calculateSearchMatches
|
||||
import org.kiwix.kiwixmobile.extensions.registerReceiver
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity
|
||||
@ -51,6 +47,7 @@ import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState
|
||||
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.CanWrite4GbFile
|
||||
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.CannotWrite4GbFile
|
||||
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.NotEnoughSpaceFor4GbFile
|
||||
import org.kiwix.kiwixmobile.zim_manager.Fat32Checker.FileSystemState.Unknown
|
||||
import org.kiwix.kiwixmobile.zim_manager.NetworkState.CONNECTED
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions.MultiModeFinished
|
||||
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions.RequestDeleteMultiSelection
|
||||
@ -80,17 +77,15 @@ import java.util.concurrent.TimeUnit.SECONDS
|
||||
import javax.inject.Inject
|
||||
|
||||
class ZimManageViewModel @Inject constructor(
|
||||
private val downloadDao: NewDownloadDao,
|
||||
private val downloadDao: FetchDownloadDao,
|
||||
private val bookDao: NewBookDao,
|
||||
private val languageDao: NewLanguagesDao,
|
||||
private val downloader: Downloader,
|
||||
private val storageObserver: StorageObserver,
|
||||
private val kiwixService: KiwixService,
|
||||
private val context: Application,
|
||||
private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver,
|
||||
private val bookUtils: BookUtils,
|
||||
private val fat32Checker: Fat32Checker,
|
||||
private val uriToFileConverter: UriToFileConverter,
|
||||
private val defaultLanguageProvider: DefaultLanguageProvider,
|
||||
private val dataSource: DataSource
|
||||
) : ViewModel() {
|
||||
@ -136,14 +131,11 @@ class ZimManageViewModel @Inject constructor(
|
||||
|
||||
private fun disposables(): Array<Disposable> {
|
||||
val downloads = downloadDao.downloads()
|
||||
val downloadStatuses = downloadStatuses(downloads)
|
||||
val booksFromDao = books()
|
||||
val networkLibrary = PublishProcessor.create<LibraryNetworkEntity>()
|
||||
val languages = languageDao.languages()
|
||||
return arrayOf(
|
||||
updateDownloadItems(downloadStatuses),
|
||||
removeCompletedDownloadsFromDb(downloadStatuses),
|
||||
removeNonExistingDownloadsFromDb(downloadStatuses, downloads),
|
||||
updateDownloadItems(downloads),
|
||||
updateBookItems(),
|
||||
checkFileSystemForBooksOnRequest(booksFromDao),
|
||||
updateLibraryItems(booksFromDao, downloads, networkLibrary, languages),
|
||||
@ -246,46 +238,6 @@ class ZimManageViewModel @Inject constructor(
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
|
||||
private fun removeNonExistingDownloadsFromDb(
|
||||
downloadStatuses: Flowable<List<DownloadStatus>>,
|
||||
downloads: Flowable<List<DownloadModel>>
|
||||
) = downloadStatuses
|
||||
.withLatestFrom(
|
||||
downloads,
|
||||
BiFunction(::combineToDownloadsWithoutStatuses)
|
||||
)
|
||||
.buffer(3, SECONDS)
|
||||
.map(::downloadIdsWithNoStatusesOverBufferPeriod)
|
||||
.filter { it.isNotEmpty() }
|
||||
.subscribe(
|
||||
{
|
||||
downloadDao.delete(*it.toLongArray())
|
||||
},
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
|
||||
private fun downloadIdsWithNoStatusesOverBufferPeriod(noStatusIds: List<MutableList<Long>>) =
|
||||
noStatusIds.flatten()
|
||||
.fold(mutableMapOf<Long, Int>(), { acc, id -> acc.increment(id) })
|
||||
.filter { (_, count) -> count == noStatusIds.size }
|
||||
.map { (id, _) -> id }
|
||||
|
||||
private fun combineToDownloadsWithoutStatuses(
|
||||
statuses: List<DownloadStatus>,
|
||||
downloads: List<DownloadModel>
|
||||
): MutableList<Long> {
|
||||
val downloadIdsWithStatuses = statuses.map(DownloadStatus::downloadId)
|
||||
return downloads.fold(
|
||||
mutableListOf(),
|
||||
{ acc, downloadModel ->
|
||||
if (!downloadIdsWithStatuses.contains(downloadModel.downloadId)) {
|
||||
acc.add(downloadModel.downloadId)
|
||||
}
|
||||
acc
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateNetworkStates() =
|
||||
connectivityBroadcastReceiver.networkStates.subscribe(
|
||||
networkStates::postValue, Throwable::printStackTrace
|
||||
@ -301,10 +253,13 @@ class ZimManageViewModel @Inject constructor(
|
||||
downloads,
|
||||
languages.filter { it.isNotEmpty() },
|
||||
library,
|
||||
requestFiltering
|
||||
.doOnNext { libraryListIsRefreshing.postValue(true) }
|
||||
.debounce(500, MILLISECONDS)
|
||||
.observeOn(Schedulers.io()),
|
||||
Flowable.merge(
|
||||
Flowable.just(""),
|
||||
requestFiltering
|
||||
.doOnNext { libraryListIsRefreshing.postValue(true) }
|
||||
.debounce(500, MILLISECONDS)
|
||||
.observeOn(Schedulers.io())
|
||||
),
|
||||
fat32Checker.fileSystemStates,
|
||||
Function6(::combineLibrarySources)
|
||||
)
|
||||
@ -400,6 +355,7 @@ class ZimManageViewModel @Inject constructor(
|
||||
libraryNetworkEntity.books
|
||||
.filter {
|
||||
when (fileSystemState) {
|
||||
Unknown,
|
||||
CannotWrite4GbFile -> isLessThan4GB(it)
|
||||
NotEnoughSpaceFor4GbFile,
|
||||
CanWrite4GbFile -> true
|
||||
@ -516,38 +472,11 @@ class ZimManageViewModel @Inject constructor(
|
||||
})
|
||||
}
|
||||
|
||||
private fun removeCompletedDownloadsFromDb(downloadStatuses: Flowable<List<DownloadStatus>>) =
|
||||
downloadStatuses
|
||||
.observeOn(Schedulers.io())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map { it.filter { status -> status.state == Successful } }
|
||||
.filter { it.isNotEmpty() }
|
||||
.subscribe(
|
||||
{
|
||||
bookDao.insert(
|
||||
it.map { downloadStatus -> downloadStatus.toBookOnDisk(uriToFileConverter) })
|
||||
downloadDao.delete(
|
||||
*it.map(DownloadStatus::downloadId).toLongArray()
|
||||
)
|
||||
},
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
|
||||
private fun updateDownloadItems(downloadStatuses: Flowable<List<DownloadStatus>>) =
|
||||
downloadStatuses
|
||||
.map { statuses -> statuses.map(::DownloadItem) }
|
||||
private fun updateDownloadItems(downloadModels: Flowable<List<DownloadModel>>) =
|
||||
downloadModels
|
||||
.map { it.map(::DownloadItem) }
|
||||
.subscribe(
|
||||
downloadItems::postValue,
|
||||
Throwable::printStackTrace
|
||||
)
|
||||
|
||||
private fun downloadStatuses(downloads: Flowable<List<DownloadModel>>) =
|
||||
Flowable.combineLatest(
|
||||
downloads,
|
||||
Flowable.interval(1, SECONDS),
|
||||
BiFunction { downloadModels: List<DownloadModel>, _: Long -> downloadModels }
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map(downloader::queryStatus)
|
||||
.distinctUntilChanged()
|
||||
}
|
||||
|
@ -3,18 +3,16 @@ package org.kiwix.kiwixmobile.zim_manager.fileselect_view
|
||||
import io.reactivex.functions.BiFunction
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.kiwix.kiwixmobile.data.ZimContentProvider
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewDownloadDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.FetchDownloadDao
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
|
||||
import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil
|
||||
import org.kiwix.kiwixmobile.utils.files.FileSearch
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class StorageObserver @Inject constructor(
|
||||
private val sharedPreferenceUtil: SharedPreferenceUtil,
|
||||
private val downloadDao: NewDownloadDao,
|
||||
private val downloadDao: FetchDownloadDao,
|
||||
private val fileSearch: FileSearch
|
||||
) {
|
||||
|
||||
@ -40,7 +38,7 @@ class StorageObserver @Inject constructor(
|
||||
file.absolutePath.endsWith(it.fileNameFromUrl)
|
||||
} == null
|
||||
|
||||
private fun scanFiles() = fileSearch.scan(sharedPreferenceUtil.prefStorage)
|
||||
private fun scanFiles() = fileSearch.scan()
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
||||
private fun convertToBookOnDisk(file: File): BookOnDisk? {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter
|
||||
|
||||
import org.kiwix.kiwixmobile.database.newdb.entities.BookOnDiskEntity
|
||||
import org.kiwix.kiwixmobile.database.newdb.entities.FetchDownloadEntity
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
@ -35,5 +36,11 @@ sealed class BooksOnDiskListItem {
|
||||
bookOnDiskEntity.toBook(),
|
||||
bookOnDiskEntity.file
|
||||
)
|
||||
|
||||
constructor(fetchDownloadEntity: FetchDownloadEntity) : this(
|
||||
0L,
|
||||
fetchDownloadEntity.toBook(),
|
||||
File(fetchDownloadEntity.file)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,8 @@
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
>
|
||||
android:paddingRight="@dimen/activity_horizontal_margin">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/favicon"
|
||||
@ -20,16 +19,14 @@
|
||||
android:adjustViewBounds="true"
|
||||
android:minHeight="?android:attr/listPreferredItemHeight"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@mipmap/kiwix_icon"
|
||||
/>
|
||||
android:src="@mipmap/kiwix_icon" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
>
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
@ -37,8 +34,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
tools:text="Title"
|
||||
/>
|
||||
tools:text="Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
@ -46,33 +42,42 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
tools:text="Description"
|
||||
/>
|
||||
tools:text="Description" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/downloadProgress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="false"
|
||||
android:padding="@dimen/download_progress_padding"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
/>
|
||||
android:padding="@dimen/download_progress_padding" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/downloadState"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
tools:text="In Progress"
|
||||
/>
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/downloadState"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
tools:text="In Progress" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/eta"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
tools:text="1min 10secs" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
>
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/stop"
|
||||
@ -82,10 +87,9 @@
|
||||
android:layout_marginLeft="@dimen/stop_horizontal_margin"
|
||||
android:layout_marginRight="@dimen/stop_horizontal_margin"
|
||||
android:layout_weight="0.5"
|
||||
android:minHeight="@dimen/stop_min_height"
|
||||
android:minWidth="@dimen/stop_min_width"
|
||||
app:srcCompat="@drawable/ic_stop_black_24dp"
|
||||
android:minHeight="@dimen/stop_min_height"
|
||||
android:text="@string/download_stop"
|
||||
/>
|
||||
app:srcCompat="@drawable/ic_stop_black_24dp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
@ -243,7 +243,7 @@
|
||||
<string name="pending_state">Pending</string>
|
||||
<string name="running_state">In Progress</string>
|
||||
<string name="successful_state">Complete</string>
|
||||
<string name="paused_state">Paused: %s</string>
|
||||
<string name="paused_state">Paused</string>
|
||||
<string name="failed_state">Failed: %s</string>
|
||||
<string name="paused_wifi">Waiting for Wifi</string>
|
||||
<string name="paused_connectivity">Waiting to connect to a network</string>
|
||||
|
@ -61,7 +61,7 @@ class FileSearchTest {
|
||||
every { Environment.getExternalStorageDirectory() } returns externalStorageDirectory
|
||||
every { externalStorageDirectory.absolutePath } returns "/externalStorageDirectory"
|
||||
every { context.contentResolver } returns contentResolver
|
||||
every { StorageDeviceUtils.getStorageDevices(context, false) } returns arrayListOf(
|
||||
every { StorageDeviceUtils.getReadableStorage(context) } returns arrayListOf(
|
||||
storageDevice
|
||||
)
|
||||
every { storageDevice.name } returns "/deviceDir"
|
||||
@ -80,7 +80,7 @@ class FileSearchTest {
|
||||
@Test
|
||||
fun `scan of directory that doesn't exist returns nothing`() {
|
||||
every { contentResolver.query(any(), any(), any(), any(), any()) } returns null
|
||||
fileSearch.scan("doesNotExist")
|
||||
fileSearch.scan()
|
||||
.test()
|
||||
.assertValue(listOf())
|
||||
}
|
||||
@ -91,7 +91,8 @@ class FileSearchTest {
|
||||
val zimaaFile = File.createTempFile("fileToFind2", ".zimaa")
|
||||
File.createTempFile("willNotFind", ".txt")
|
||||
every { contentResolver.query(any(), any(), any(), any(), any()) } returns null
|
||||
val fileList = fileSearch.scan(zimFile.parent)
|
||||
every { storageDevice.name } returns zimFile.parent
|
||||
val fileList = fileSearch.scan()
|
||||
.test()
|
||||
.values()[0]
|
||||
assertThat(fileList).containsExactlyInAnyOrder(zimFile, zimaaFile)
|
||||
@ -106,7 +107,8 @@ class FileSearchTest {
|
||||
".zim",
|
||||
File("$tempRoot${File.separator}dir").apply { mkdirs() })
|
||||
every { contentResolver.query(any(), any(), any(), any(), any()) } returns null
|
||||
val fileList = fileSearch.scan(zimFile.parentFile.parent)
|
||||
every { storageDevice.name } returns zimFile.parentFile.parent
|
||||
val fileList = fileSearch.scan()
|
||||
.test()
|
||||
.values()[0]
|
||||
assertThat(fileList).containsExactlyInAnyOrder(zimFile)
|
||||
@ -120,7 +122,7 @@ class FileSearchTest {
|
||||
fun `scan media store, if files are readable they are returned`() {
|
||||
val fileToFind = File.createTempFile("fileToFind", ".zim")
|
||||
expectFromMediaStore(fileToFind)
|
||||
fileSearch.scan("")
|
||||
fileSearch.scan()
|
||||
.test()
|
||||
.assertValue(listOf(fileToFind))
|
||||
}
|
||||
@ -130,7 +132,7 @@ class FileSearchTest {
|
||||
val unreadableFile = File.createTempFile("fileToFind", ".zim")
|
||||
expectFromMediaStore(unreadableFile)
|
||||
unreadableFile.delete()
|
||||
fileSearch.scan("")
|
||||
fileSearch.scan()
|
||||
.test()
|
||||
.assertValue(listOf())
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.processors.BehaviorProcessor
|
||||
import io.reactivex.processors.PublishProcessor
|
||||
import io.reactivex.schedulers.TestScheduler
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
@ -38,17 +39,12 @@ import org.kiwix.kiwixmobile.book
|
||||
import org.kiwix.kiwixmobile.bookOnDisk
|
||||
import org.kiwix.kiwixmobile.data.DataSource
|
||||
import org.kiwix.kiwixmobile.data.remote.KiwixService
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.FetchDownloadDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewBookDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewDownloadDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewLanguagesDao
|
||||
import org.kiwix.kiwixmobile.downloadItem
|
||||
import org.kiwix.kiwixmobile.downloadModel
|
||||
import org.kiwix.kiwixmobile.downloadStatus
|
||||
import org.kiwix.kiwixmobile.downloader.Downloader
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadItem
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadState
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadStatus
|
||||
import org.kiwix.kiwixmobile.downloader.model.UriToFileConverter
|
||||
import org.kiwix.kiwixmobile.language
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
|
||||
import org.kiwix.kiwixmobile.libraryNetworkEntity
|
||||
@ -65,7 +61,6 @@ import org.kiwix.kiwixmobile.zim_manager.fileselect_view.StorageObserver
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem
|
||||
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
|
||||
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
@ -73,17 +68,15 @@ import java.util.concurrent.TimeUnit.SECONDS
|
||||
@ExtendWith(InstantExecutorExtension::class)
|
||||
class ZimManageViewModelTest {
|
||||
|
||||
private val newDownloadDao: NewDownloadDao = mockk()
|
||||
private val downloadDao: FetchDownloadDao = mockk()
|
||||
private val newBookDao: NewBookDao = mockk()
|
||||
private val newLanguagesDao: NewLanguagesDao = mockk()
|
||||
private val downloader: Downloader = mockk()
|
||||
private val storageObserver: StorageObserver = mockk()
|
||||
private val kiwixService: KiwixService = mockk()
|
||||
private val application: Application = mockk()
|
||||
private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver = mockk()
|
||||
private val bookUtils: BookUtils = mockk()
|
||||
private val fat32Checker: Fat32Checker = mockk()
|
||||
private val uriToFileConverter: UriToFileConverter = mockk()
|
||||
private val defaultLanguageProvider: DefaultLanguageProvider = mockk()
|
||||
private val dataSource: DataSource = mockk()
|
||||
lateinit var viewModel: ZimManageViewModel
|
||||
@ -92,7 +85,7 @@ class ZimManageViewModelTest {
|
||||
private val booksOnFileSystem: PublishProcessor<List<BookOnDisk>> = PublishProcessor.create()
|
||||
private val books: PublishProcessor<List<BookOnDisk>> = PublishProcessor.create()
|
||||
private val languages: PublishProcessor<List<Language>> = PublishProcessor.create()
|
||||
private val fileSystemStates: PublishProcessor<FileSystemState> = PublishProcessor.create()
|
||||
private val fileSystemStates: BehaviorProcessor<FileSystemState> = BehaviorProcessor.create()
|
||||
private val networkStates: PublishProcessor<NetworkState> = PublishProcessor.create()
|
||||
private val booksOnDiskListItems: PublishProcessor<List<BooksOnDiskListItem>> =
|
||||
PublishProcessor.create()
|
||||
@ -112,7 +105,7 @@ class ZimManageViewModelTest {
|
||||
fun init() {
|
||||
clearAllMocks()
|
||||
every { connectivityBroadcastReceiver.action } returns "test"
|
||||
every { newDownloadDao.downloads() } returns downloads
|
||||
every { downloadDao.downloads() } returns downloads
|
||||
every { newBookDao.books() } returns books
|
||||
every { storageObserver.booksOnFileSystem } returns booksOnFileSystem
|
||||
every { newLanguagesDao.languages() } returns languages
|
||||
@ -121,9 +114,17 @@ class ZimManageViewModelTest {
|
||||
every { application.registerReceiver(any(), any()) } returns mockk()
|
||||
every { dataSource.booksOnDiskAsListItems() } returns booksOnDiskListItems
|
||||
viewModel = ZimManageViewModel(
|
||||
newDownloadDao, newBookDao, newLanguagesDao, downloader,
|
||||
storageObserver, kiwixService, application, connectivityBroadcastReceiver, bookUtils,
|
||||
fat32Checker, uriToFileConverter, defaultLanguageProvider, dataSource
|
||||
downloadDao,
|
||||
newBookDao,
|
||||
newLanguagesDao,
|
||||
storageObserver,
|
||||
kiwixService,
|
||||
application,
|
||||
connectivityBroadcastReceiver,
|
||||
bookUtils,
|
||||
fat32Checker,
|
||||
defaultLanguageProvider,
|
||||
dataSource
|
||||
)
|
||||
testScheduler.triggerActions()
|
||||
}
|
||||
@ -150,63 +151,19 @@ class ZimManageViewModelTest {
|
||||
@Nested
|
||||
inner class Downloads {
|
||||
@Test
|
||||
fun `on emission from database query and render downloads`() {
|
||||
val expectedStatus = downloadStatus()
|
||||
expectStatusWith(listOf(expectedStatus))
|
||||
fun `on emission from database render downloads`() {
|
||||
expectDownloads()
|
||||
viewModel.downloadItems
|
||||
.test()
|
||||
.assertValue(listOf(DownloadItem(expectedStatus)))
|
||||
.assertValue(listOf(downloadItem()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on emission of successful status create a book and delete the download`() {
|
||||
every { uriToFileConverter.convert(any()) } returns File("test")
|
||||
val expectedStatus = downloadStatus(
|
||||
downloadId = 10L,
|
||||
downloadState = DownloadState.Successful
|
||||
)
|
||||
expectStatusWith(listOf(expectedStatus))
|
||||
val element = expectedStatus.toBookOnDisk(uriToFileConverter)
|
||||
verify {
|
||||
newBookDao.insert(listOf(element))
|
||||
newDownloadDao.delete(10L)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if statuses don't have a matching Id for download in db over 3 secs then delete`() {
|
||||
expectStatusWith(
|
||||
listOf(downloadStatus(downloadId = 1)),
|
||||
listOf(downloadModel(downloadId = 1), downloadModel(downloadId = 3))
|
||||
)
|
||||
testScheduler.advanceTimeBy(3, SECONDS)
|
||||
testScheduler.triggerActions()
|
||||
verify {
|
||||
newDownloadDao.delete(3)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `if statuses do have a matching Id for download in db over 3 secs then don't delete`() {
|
||||
expectStatusWith(
|
||||
listOf(downloadStatus(downloadId = 1)),
|
||||
listOf(downloadModel(downloadId = 1))
|
||||
)
|
||||
testScheduler.advanceTimeBy(3, SECONDS)
|
||||
testScheduler.triggerActions()
|
||||
verify(exactly = 0) {
|
||||
newDownloadDao.delete(any())
|
||||
}
|
||||
}
|
||||
|
||||
private fun expectStatusWith(
|
||||
expectedStatuses: List<DownloadStatus>,
|
||||
private fun expectDownloads(
|
||||
expectedDownloads: List<DownloadModel> = listOf(
|
||||
downloadModel()
|
||||
)
|
||||
) {
|
||||
every { application.getString(any()) } returns ""
|
||||
every { downloader.queryStatus(expectedDownloads) } returns expectedStatuses
|
||||
downloads.offer(expectedDownloads)
|
||||
testScheduler.triggerActions()
|
||||
testScheduler.advanceTimeBy(1, SECONDS)
|
||||
@ -387,7 +344,6 @@ class ZimManageViewModelTest {
|
||||
|
||||
@Test
|
||||
fun `library update removes from sources`() {
|
||||
every { downloader.queryStatus(any()) } returns emptyList()
|
||||
every { application.getString(R.string.your_languages) } returns "1"
|
||||
every { application.getString(R.string.other_languages) } returns "2"
|
||||
val bookAlreadyOnDisk = book(
|
||||
|
@ -32,7 +32,7 @@ import org.junit.jupiter.api.Test
|
||||
import org.kiwix.kiwixmobile.book
|
||||
import org.kiwix.kiwixmobile.bookOnDisk
|
||||
import org.kiwix.kiwixmobile.data.ZimContentProvider
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.NewDownloadDao
|
||||
import org.kiwix.kiwixmobile.database.newdb.dao.FetchDownloadDao
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
|
||||
import org.kiwix.kiwixmobile.resetSchedulers
|
||||
@ -44,7 +44,7 @@ import java.io.File
|
||||
class StorageObserverTest {
|
||||
|
||||
private val sharedPreferenceUtil: SharedPreferenceUtil = mockk()
|
||||
private val newDownloadDao: NewDownloadDao = mockk()
|
||||
private val downloadDao: FetchDownloadDao = mockk()
|
||||
private val fileSearch: FileSearch = mockk()
|
||||
private val downloadModel = mockk<DownloadModel>()
|
||||
private val file = mockk<File>()
|
||||
@ -66,9 +66,9 @@ class StorageObserverTest {
|
||||
@BeforeEach fun init() {
|
||||
clearAllMocks()
|
||||
every { sharedPreferenceUtil.prefStorage } returns "a"
|
||||
every { fileSearch.scan("a") } returns files
|
||||
every { newDownloadDao.downloads() } returns downloads
|
||||
storageObserver = StorageObserver(sharedPreferenceUtil, newDownloadDao, fileSearch)
|
||||
every { fileSearch.scan() } returns files
|
||||
every { downloadDao.downloads() } returns downloads
|
||||
storageObserver = StorageObserver(downloadDao, fileSearch)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -17,10 +17,15 @@
|
||||
*/
|
||||
package org.kiwix.kiwixmobile
|
||||
|
||||
import com.tonyodev.fetch2.Error
|
||||
import com.tonyodev.fetch2.Status
|
||||
import com.tonyodev.fetch2.Status.NONE
|
||||
import org.kiwix.kiwixmobile.downloader.model.Base64String
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadItem
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadModel
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadState
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadState.Pending
|
||||
import org.kiwix.kiwixmobile.downloader.model.DownloadStatus
|
||||
import org.kiwix.kiwixmobile.downloader.model.Seconds
|
||||
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.LanguageItem
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity
|
||||
import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book
|
||||
@ -39,29 +44,37 @@ fun bookOnDisk(
|
||||
file: File = File("")
|
||||
) = BookOnDisk(databaseId, book, file)
|
||||
|
||||
fun downloadStatus(
|
||||
downloadId: Long = 0L,
|
||||
title: String = "",
|
||||
description: String = "",
|
||||
downloadState: DownloadState = Pending,
|
||||
bytesDownloadedSoFar: Long = 0L,
|
||||
totalSizeBytes: Long = 0L,
|
||||
lastModified: String = "",
|
||||
localUri: String? = null,
|
||||
mediaProviderUri: String? = null,
|
||||
mediaType: String? = null,
|
||||
uri: String? = null,
|
||||
fun downloadModel(
|
||||
databaseId: Long = 1L,
|
||||
downloadId: Long = 1L,
|
||||
file: String = "",
|
||||
etaInMilliSeconds: Long = 0L,
|
||||
bytesDownloaded: Long = 1L,
|
||||
totalSizeOfDownload: Long = 1L,
|
||||
status: Status = NONE,
|
||||
error: Error = Error.NONE,
|
||||
progress: Int = 1,
|
||||
book: Book = book()
|
||||
) = DownloadStatus(
|
||||
downloadId, title, description, downloadState, bytesDownloadedSoFar,
|
||||
totalSizeBytes, lastModified, localUri, mediaProviderUri, mediaType, uri, book
|
||||
) = DownloadModel(
|
||||
databaseId, downloadId, file, etaInMilliSeconds, bytesDownloaded, totalSizeOfDownload,
|
||||
status, error, progress, book
|
||||
)
|
||||
|
||||
fun downloadModel(
|
||||
databaseId: Long? = 1L,
|
||||
fun downloadItem(
|
||||
downloadId: Long = 1L,
|
||||
book: Book = book()
|
||||
) = DownloadModel(databaseId, downloadId, book)
|
||||
favIcon: Base64String = Base64String("favIcon"),
|
||||
title: String = "title",
|
||||
description: String = "description",
|
||||
bytesDownloaded: Long = 1L,
|
||||
totalSizeBytes: Long = 1L,
|
||||
progress: Int = 1,
|
||||
eta: Seconds = Seconds(0),
|
||||
state: DownloadState = Pending
|
||||
) =
|
||||
DownloadItem(
|
||||
downloadId, favIcon, title, description, bytesDownloaded,
|
||||
totalSizeBytes, progress, eta, state
|
||||
)
|
||||
|
||||
fun language(
|
||||
id: Long = 0,
|
||||
|
@ -35,8 +35,9 @@ ext {
|
||||
set("powerMockVersion", "1.6.6")
|
||||
set("powerMockJUnitVersion", "1.7.4")
|
||||
set("baristaVersion", "2.7.1")
|
||||
set("kotlinVersion", "1.3.41")
|
||||
set("kotlinVersion", "1.3.50")
|
||||
set("objectboxVersion", "2.3.4")
|
||||
set("fetchVersion", "3.1.4")
|
||||
}
|
||||
|
||||
allprojects {
|
||||
|
@ -1 +1 @@
|
||||
include(":app", ":kiwixlib")
|
||||
include(":app")
|
||||
|
Loading…
x
Reference in New Issue
Block a user