Merge pull request #1980 from kiwix/macgills/feature/1659-autoload-next-video

#1659 Autoload next video - replace content provider with WebResource…
This commit is contained in:
Seán Mac Gillicuddy 2020-04-22 16:58:28 +01:00 committed by GitHub
commit d766ec7e90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 162 additions and 260 deletions

View File

@ -364,7 +364,9 @@
<inspection_tool class="ReplaceSizeCheckWithIsNotEmpty" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="ReplaceSizeCheckWithIsNotEmpty" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ReplaceSizeZeroCheckWithIsEmpty" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="ReplaceSizeZeroCheckWithIsEmpty" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ReplaceStringFormatWithLiteral" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="ReplaceStringFormatWithLiteral" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ReplaceToStringWithStringTemplate" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="ReplaceToStringWithStringTemplate" enabled="true" level="ERROR" enabled_by_default="true">
<scope name="Tests" level="INFORMATION" enabled="true" />
</inspection_tool>
<inspection_tool class="ReplaceToWithInfixForm" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="ReplaceToWithInfixForm" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ReplaceWithEnumMap" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="ReplaceWithEnumMap" enabled="true" level="ERROR" enabled_by_default="true" />
<inspection_tool class="ReplaceWithOperatorAssignment" enabled="true" level="ERROR" enabled_by_default="true" /> <inspection_tool class="ReplaceWithOperatorAssignment" enabled="true" level="ERROR" enabled_by_default="true" />

View File

@ -39,7 +39,6 @@ import org.junit.runner.RunWith;
import org.kiwix.kiwixmobile.core.CoreApp; import org.kiwix.kiwixmobile.core.CoreApp;
import org.kiwix.kiwixmobile.core.di.components.DaggerTestComponent; import org.kiwix.kiwixmobile.core.di.components.DaggerTestComponent;
import org.kiwix.kiwixmobile.core.di.components.TestComponent; import org.kiwix.kiwixmobile.core.di.components.TestComponent;
import org.kiwix.kiwixmobile.core.reader.ZimContentProvider;
import org.kiwix.kiwixmobile.main.KiwixMainActivity; import org.kiwix.kiwixmobile.main.KiwixMainActivity;
import org.kiwix.kiwixmobile.testutils.TestUtils; import org.kiwix.kiwixmobile.testutils.TestUtils;
import org.kiwix.kiwixmobile.utils.KiwixIdlingResource; import org.kiwix.kiwixmobile.utils.KiwixIdlingResource;
@ -92,8 +91,6 @@ public class NetworkTest {
CoreApp.setCoreComponent(component); CoreApp.setCoreComponent(component);
ZimContentProvider zimContentProvider = new ZimContentProvider();
CoreApp.getCoreComponent().inject(zimContentProvider);
component.inject(this); component.inject(this);
InputStream library = NetworkTest.class.getClassLoader().getResourceAsStream("library.xml"); InputStream library = NetworkTest.class.getClassLoader().getResourceAsStream("library.xml");
InputStream metalinks = InputStream metalinks =

View File

@ -6,9 +6,6 @@ buildscript {
dependencies { dependencies {
classpath(Libs.com_android_tools_build_gradle) classpath(Libs.com_android_tools_build_gradle)
classpath(Libs.kotlin_gradle_plugin) classpath(Libs.kotlin_gradle_plugin)
classpath(Libs.ktlint_gradle)
classpath(Libs.jacoco_android)
classpath(Libs.detekt_gradle_plugin)
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
} }

View File

@ -89,7 +89,7 @@ object Versions {
const val core_ktx: String = "1.2.0" const val core_ktx: String = "1.2.0"
const val kiwixlib: String = "9.1.0" const val kiwixlib: String = "9.1.2"
const val material: String = "1.1.0-beta02" // available: "1.1.0" const val material: String = "1.1.0-beta02" // available: "1.1.0"

View File

@ -4,6 +4,7 @@
<Whitelist> <Whitelist>
<ID>EmptyFunctionBlock:BooksOnDiskViewHolder.kt$BookOnDiskViewHolder.BookViewHolder${ }</ID> <ID>EmptyFunctionBlock:BooksOnDiskViewHolder.kt$BookOnDiskViewHolder.BookViewHolder${ }</ID>
<ID>EmptyFunctionBlock:FetchDownloadMonitor.kt$FetchDownloadMonitor.&lt;no name provided&gt;${}</ID> <ID>EmptyFunctionBlock:FetchDownloadMonitor.kt$FetchDownloadMonitor.&lt;no name provided&gt;${}</ID>
<ID>ForbiddenComment:JNIInitialiser.kt$JNIInitialiser$// TODO: Consider surfacing to user</ID>
<ID>LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList&lt;KiwixWebView&gt;, urlIsValid: Boolean, menuClickListener: MenuClickListener, disableReadAloud: Boolean, disableTabs: Boolean )</ID> <ID>LongParameterList:MainMenu.kt$MainMenu.Factory$( menu: Menu, webViews: MutableList&lt;KiwixWebView&gt;, urlIsValid: Boolean, menuClickListener: MenuClickListener, disableReadAloud: Boolean, disableTabs: Boolean )</ID>
<ID>MagicNumber:ArticleCount.kt$ArticleCount$1000.0</ID> <ID>MagicNumber:ArticleCount.kt$ArticleCount$1000.0</ID>
<ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID> <ID>MagicNumber:ArticleCount.kt$ArticleCount$3</ID>
@ -12,6 +13,7 @@
<ID>MagicNumber:DownloaderModule.kt$DownloaderModule$5</ID> <ID>MagicNumber:DownloaderModule.kt$DownloaderModule$5</ID>
<ID>MagicNumber:FetchDownloadRequester.kt$10</ID> <ID>MagicNumber:FetchDownloadRequester.kt$10</ID>
<ID>MagicNumber:FileUtils.kt$FileUtils$3</ID> <ID>MagicNumber:FileUtils.kt$FileUtils$3</ID>
<ID>MagicNumber:JNIInitialiser.kt$JNIInitialiser$1024</ID>
<ID>MagicNumber:KiloByte.kt$KiloByte$1024.0</ID> <ID>MagicNumber:KiloByte.kt$KiloByte$1024.0</ID>
<ID>MagicNumber:MainMenu.kt$MainMenu$99</ID> <ID>MagicNumber:MainMenu.kt$MainMenu$99</ID>
<ID>MagicNumber:SearchResultGenerator.kt$ZimSearchResultGenerator$200</ID> <ID>MagicNumber:SearchResultGenerator.kt$ZimSearchResultGenerator$200</ID>
@ -20,6 +22,7 @@
<ID>MagicNumber:Seconds.kt$Seconds$60.0</ID> <ID>MagicNumber:Seconds.kt$Seconds$60.0</ID>
<ID>NestedBlockDepth:FileUtils.kt$FileUtils$deleteZimFile</ID> <ID>NestedBlockDepth:FileUtils.kt$FileUtils$deleteZimFile</ID>
<ID>NestedBlockDepth:ImageUtils.kt$ImageUtils$getBitmapFromView</ID> <ID>NestedBlockDepth:ImageUtils.kt$ImageUtils$getBitmapFromView</ID>
<ID>NestedBlockDepth:JNIInitialiser.kt$JNIInitialiser$loadICUData</ID>
<ID>NestedBlockDepth:StorageDeviceUtils.kt$StorageDeviceUtils$canWrite</ID> <ID>NestedBlockDepth:StorageDeviceUtils.kt$StorageDeviceUtils$canWrite</ID>
<ID>PackageNaming:ArticleCount.kt$package org.kiwix.kiwixmobile.core.zim_manager.fileselect_view</ID> <ID>PackageNaming:ArticleCount.kt$package org.kiwix.kiwixmobile.core.zim_manager.fileselect_view</ID>
<ID>PackageNaming:BookOnDiskDelegate.kt$package org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter</ID> <ID>PackageNaming:BookOnDiskDelegate.kt$package org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter</ID>
@ -36,6 +39,7 @@
<ID>ReturnCount:FileUtils.kt$FileUtils$@JvmStatic fun hasPart(file: File): Boolean</ID> <ID>ReturnCount:FileUtils.kt$FileUtils$@JvmStatic fun hasPart(file: File): Boolean</ID>
<ID>ReturnCount:FileUtils.kt$FileUtils$@Synchronized private fun deleteZimFileParts(path: String): Boolean</ID> <ID>ReturnCount:FileUtils.kt$FileUtils$@Synchronized private fun deleteZimFileParts(path: String): Boolean</ID>
<ID>ReturnCount:ImageUtils.kt$ImageUtils$private fun getBitmapFromView(width: Int, height: Int, viewToDrawFrom: View): Bitmap?</ID> <ID>ReturnCount:ImageUtils.kt$ImageUtils$private fun getBitmapFromView(width: Int, height: Int, viewToDrawFrom: View): Bitmap?</ID>
<ID>TooGenericExceptionCaught:JNIInitialiser.kt$JNIInitialiser$e: Exception</ID>
<ID>TooGenericExceptionThrown:AbstractContentProvider.kt$AbstractContentProvider$throw RuntimeException("Operation not supported")</ID> <ID>TooGenericExceptionThrown:AbstractContentProvider.kt$AbstractContentProvider$throw RuntimeException("Operation not supported")</ID>
<ID>TooGenericExceptionThrown:AdapterDelegateManager.kt$AdapterDelegateManager$throw RuntimeException("No delegate registered for $item")</ID> <ID>TooGenericExceptionThrown:AdapterDelegateManager.kt$AdapterDelegateManager$throw RuntimeException("No delegate registered for $item")</ID>
<ID>TooGenericExceptionThrown:Bytes.kt$Bytes$throw RuntimeException("impossible value $size")</ID> <ID>TooGenericExceptionThrown:Bytes.kt$Bytes$throw RuntimeException("impossible value $size")</ID>
@ -51,7 +55,6 @@
<ID>TooManyFunctions:NewBookDao.kt$NewBookDao$NewBookDao</ID> <ID>TooManyFunctions:NewBookDao.kt$NewBookDao$NewBookDao</ID>
<ID>TooManyFunctions:Repository.kt$Repository$Repository</ID> <ID>TooManyFunctions:Repository.kt$Repository$Repository</ID>
<ID>TooManyFunctions:ZimFileReader.kt$ZimFileReader$ZimFileReader</ID> <ID>TooManyFunctions:ZimFileReader.kt$ZimFileReader$ZimFileReader</ID>
<ID>TooManyFunctions:ZimReaderContainer.kt$ZimReaderContainer$ZimReaderContainer</ID>
<ID>TopLevelPropertyNaming:Bytes.kt$const val Eb = Pb * 1024</ID> <ID>TopLevelPropertyNaming:Bytes.kt$const val Eb = Pb * 1024</ID>
<ID>TopLevelPropertyNaming:Bytes.kt$const val Gb = Mb * 1024</ID> <ID>TopLevelPropertyNaming:Bytes.kt$const val Gb = Mb * 1024</ID>
<ID>TopLevelPropertyNaming:Bytes.kt$const val Kb = 1 * 1024L</ID> <ID>TopLevelPropertyNaming:Bytes.kt$const val Kb = 1 * 1024L</ID>

View File

@ -29,11 +29,6 @@
<activity android:name=".bookmark.BookmarksActivity" /> <activity android:name=".bookmark.BookmarksActivity" />
<provider
android:name=".reader.ZimContentProvider"
android:authorities="${applicationId}.zim.base"
android:exported="true" />
<activity <activity
android:name=".error.ErrorActivity" android:name=".error.ErrorActivity"
android:process=":error_activity" /> android:process=":error_activity" />

View File

@ -50,6 +50,14 @@ public abstract class CoreApp extends Application {
@Inject @Inject
KiwixDatabase kiwixDatabase; KiwixDatabase kiwixDatabase;
/**
* The init of this class does the work of initializing,
* simply injecting it is all that there is to be done
*/
@SuppressWarnings("unused")
@Inject
JNIInitialiser jniInitialiser;
public static CoreApp getInstance() { public static CoreApp getInstance() {
return app; return app;
} }

View File

@ -0,0 +1,57 @@
/*
* Kiwix Android
* Copyright (c) 2020 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.core
import android.content.Context
import android.util.Log
import org.kiwix.kiwixlib.JNIKiwix
import org.kiwix.kiwixmobile.core.utils.TAG_KIWIX
import java.io.File
import java.io.FileOutputStream
import javax.inject.Inject
internal class JNIInitialiser @Inject constructor(context: Context, jniKiwix: JNIKiwix) {
init {
loadICUData(context)?.let(jniKiwix::setDataDirectory)
}
private fun loadICUData(context: Context): String? {
return try {
val icuDir = File(context.filesDir, "icu")
if (!icuDir.exists()) {
icuDir.mkdirs()
}
val icuFileNames = context.assets.list("icu") ?: emptyArray()
for (icuFileName in icuFileNames) {
val icuDataFile = File(icuDir, icuFileName)
if (!icuDataFile.exists()) {
FileOutputStream(icuDataFile).use { outputStream ->
context.assets.open("icu/$icuFileName").use { inputStream ->
inputStream.copyTo(outputStream, 1024)
}
}
}
}
icuDir.absolutePath
} catch (e: Exception) {
Log.w(TAG_KIWIX, "Error copying icu data file", e)
// TODO: Consider surfacing to user
null
}
}
}

View File

@ -1,53 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 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.core.data
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.net.Uri
internal abstract class AbstractContentProvider : ContentProvider() {
override fun query(
url: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sort: String?
): Cursor? {
throw RuntimeException("Operation not supported")
}
override fun insert(uri: Uri, initialValues: ContentValues?): Uri? {
throw RuntimeException("Operation not supported")
}
override fun update(
uri: Uri,
values: ContentValues?,
where: String?,
whereArgs: Array<String>?
): Int {
throw RuntimeException("Operation not supported")
}
override fun delete(uri: Uri, where: String?, whereArgs: Array<String>?): Int {
throw RuntimeException("Operation not supported")
}
}

View File

@ -47,7 +47,6 @@ import org.kiwix.kiwixmobile.core.error.ErrorActivity
import org.kiwix.kiwixmobile.core.help.HelpActivity import org.kiwix.kiwixmobile.core.help.HelpActivity
import org.kiwix.kiwixmobile.core.history.HistoryModule import org.kiwix.kiwixmobile.core.history.HistoryModule
import org.kiwix.kiwixmobile.core.main.KiwixWebView import org.kiwix.kiwixmobile.core.main.KiwixWebView
import org.kiwix.kiwixmobile.core.reader.ZimContentProvider
import org.kiwix.kiwixmobile.core.reader.ZimFileReader import org.kiwix.kiwixmobile.core.reader.ZimFileReader
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.search.SearchActivity import org.kiwix.kiwixmobile.core.search.SearchActivity
@ -101,7 +100,6 @@ interface CoreComponent {
fun notificationManager(): NotificationManager fun notificationManager(): NotificationManager
fun inject(application: CoreApp) fun inject(application: CoreApp)
fun inject(zimContentProvider: ZimContentProvider)
fun inject(kiwixWebView: KiwixWebView) fun inject(kiwixWebView: KiwixWebView)
fun inject(storageSelectDialog: StorageSelectDialog) fun inject(storageSelectDialog: StorageSelectDialog)

View File

@ -1391,7 +1391,7 @@ public abstract class CoreMainActivity extends BaseActivity
@NotNull @NotNull
private String contentUrl(String articleUrl) { private String contentUrl(String articleUrl) {
return Uri.parse(ZimFileReader.CONTENT_URI + articleUrl).toString(); return Uri.parse(ZimFileReader.CONTENT_PREFIX + articleUrl).toString();
} }
@NotNull @NotNull
@ -1680,7 +1680,7 @@ public abstract class CoreMainActivity extends BaseActivity
@Override @Override
public void webViewLongClick(final String url) { public void webViewLongClick(final String url) {
boolean handleEvent = false; boolean handleEvent = false;
if (url.startsWith(ZimFileReader.CONTENT_URI.toString())) { if (url.startsWith(ZimFileReader.CONTENT_PREFIX)) {
// This is my web site, so do not override; let my WebView load the page // This is my web site, so do not override; let my WebView load the page
handleEvent = true; handleEvent = true;
} else if (url.startsWith("file://")) { } else if (url.startsWith("file://")) {

View File

@ -24,16 +24,19 @@ import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import android.webkit.WebResourceResponse;
import android.webkit.WebView; import android.webkit.WebView;
import android.webkit.WebViewClient; import android.webkit.WebViewClient;
import androidx.annotation.Nullable;
import java.util.HashMap; import java.util.HashMap;
import org.kiwix.kiwixmobile.core.CoreApp; import org.kiwix.kiwixmobile.core.CoreApp;
import org.kiwix.kiwixmobile.core.R; import org.kiwix.kiwixmobile.core.R;
import org.kiwix.kiwixmobile.core.reader.ZimFileReader;
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer; import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer;
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil; import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil;
import static org.kiwix.kiwixmobile.core.main.CoreMainActivity.HOME_URL; import static org.kiwix.kiwixmobile.core.main.CoreMainActivity.HOME_URL;
import static org.kiwix.kiwixmobile.core.reader.ZimFileReader.CONTENT_PREFIX;
import static org.kiwix.kiwixmobile.core.reader.ZimFileReader.UI_URI;
import static org.kiwix.kiwixmobile.core.utils.ConstantsKt.EXTRA_EXTERNAL_LINK; import static org.kiwix.kiwixmobile.core.utils.ConstantsKt.EXTRA_EXTERNAL_LINK;
import static org.kiwix.kiwixmobile.core.utils.ConstantsKt.TAG_KIWIX; import static org.kiwix.kiwixmobile.core.utils.ConstantsKt.TAG_KIWIX;
@ -65,7 +68,7 @@ public abstract class CoreWebViewClient extends WebViewClient {
view.loadUrl(zimReaderContainer.getRedirect(url)); view.loadUrl(zimReaderContainer.getRedirect(url));
return true; return true;
} }
if (url.startsWith(ZimFileReader.CONTENT_URI.toString())) { if (url.startsWith(CONTENT_PREFIX)) {
return handleEpubAndPdf(url); return handleEpubAndPdf(url);
} }
if (url.startsWith("file://")) { if (url.startsWith("file://")) {
@ -76,7 +79,7 @@ public abstract class CoreWebViewClient extends WebViewClient {
// Allow javascript for HTML functions and code execution (EX: night mode) // Allow javascript for HTML functions and code execution (EX: night mode)
return true; return true;
} }
if (url.startsWith(ZimFileReader.UI_URI.toString())) { if (url.startsWith(UI_URI.toString())) {
Log.e("KiwixWebViewClient", "UI Url " + url + " not supported."); Log.e("KiwixWebViewClient", "UI Url " + url + " not supported.");
//TODO: Document this code - what's a UI_URL? //TODO: Document this code - what's a UI_URL?
return true; return true;
@ -137,4 +140,14 @@ public abstract class CoreWebViewClient extends WebViewClient {
view.removeAllViews(); view.removeAllViews();
view.addView(home); view.addView(home);
} }
@Nullable
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
if (url.startsWith(CONTENT_PREFIX)) {
return zimReaderContainer.load(url);
} else {
return super.shouldInterceptRequest(view, url);
}
}
} }

View File

@ -1,120 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 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.core.reader;
import android.content.Context;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import javax.inject.Inject;
import org.kiwix.kiwixlib.JNIKiwix;
import org.kiwix.kiwixmobile.core.CoreApp;
import org.kiwix.kiwixmobile.core.data.AbstractContentProvider;
import static org.kiwix.kiwixmobile.core.utils.ConstantsKt.TAG_KIWIX;
public class ZimContentProvider extends AbstractContentProvider {
@Inject
public JNIKiwix jniKiwix;
@Inject
ZimReaderContainer zimReaderContainer;
@Override
public boolean onCreate() {
CoreApp.getCoreComponent().inject(this);
setIcuDataDirectory();
return true;
}
@Override
public String getType(Uri uri) {
return zimReaderContainer.readMimeType(uri);
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) {
return zimReaderContainer.load(uri);
}
private void setIcuDataDirectory() {
String icuDirPath = loadICUData(getContext());
if (icuDirPath != null) {
Log.d(TAG_KIWIX, "Setting the ICU directory path to " + icuDirPath);
jniKiwix.setDataDirectory(icuDirPath);
}
}
private String loadICUData(Context context) {
try {
File icuDir = new File(context.getFilesDir(), "icu");
if (!icuDir.exists()) {
icuDir.mkdirs();
}
String[] icuFileNames = context.getAssets().list("icu");
for (int i = 0; i < icuFileNames.length; i++) {
String icuFileName = icuFileNames[i];
File icuDataFile = new File(icuDir, icuFileName);
if (!icuDataFile.exists()) {
InputStream in = context.getAssets().open("icu/" + icuFileName);
OutputStream out = new FileOutputStream(icuDataFile);
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
in.close();
out.flush();
out.close();
}
}
return icuDir.getAbsolutePath();
} catch (Exception e) {
Log.w(TAG_KIWIX, "Error copying icu data file", e);
//TODO: Consider surfacing to user
return null;
}
}
private static String getFulltextIndexPath(String file) {
String[] names = { file, file };
/* File might be a ZIM chunk like foobar.zimaa */
if (!names[0].substring(names[0].length() - 3).equals("zim")) {
names[0] = names[0].substring(0, names[0].length() - 2);
}
/* Try to find a *.idx fulltext file/directory beside the ZIM
* file. Returns <zimfile>.zim.idx or <zimfile>.zimaa.idx. */
for (String name : names) {
File f = new File(name + ".idx");
if (f.exists() && f.isDirectory()) {
return f.getPath();
}
}
/* If no separate fulltext index file found then returns the ZIM
* file path itself (embedded fulltext index) */
return file;
}
}

View File

@ -17,14 +17,13 @@
*/ */
package org.kiwix.kiwixmobile.core.reader package org.kiwix.kiwixmobile.core.reader
import android.content.res.AssetFileDescriptor
import android.net.Uri import android.net.Uri
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.os.ParcelFileDescriptor.AutoCloseOutputStream
import android.os.ParcelFileDescriptor.dup
import android.util.Log import android.util.Log
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import androidx.core.net.toUri import androidx.core.net.toUri
import io.reactivex.Single import io.reactivex.Completable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import org.kiwix.kiwixlib.JNIKiwixException import org.kiwix.kiwixlib.JNIKiwixException
import org.kiwix.kiwixlib.JNIKiwixInt import org.kiwix.kiwixlib.JNIKiwixInt
@ -34,14 +33,16 @@ import org.kiwix.kiwixlib.Pair
import org.kiwix.kiwixmobile.core.CoreApp import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.NightModeConfig import org.kiwix.kiwixmobile.core.NightModeConfig
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Companion.CONTENT_URI import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Companion.CONTENT_PREFIX
import org.kiwix.kiwixmobile.core.search.SearchSuggestion import org.kiwix.kiwixmobile.core.search.SearchSuggestion
import org.kiwix.kiwixmobile.core.utils.files.FileUtils import org.kiwix.kiwixmobile.core.utils.files.FileUtils
import java.io.File import java.io.File
import java.io.FileDescriptor import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.io.RandomAccessFile import java.io.InputStream
import java.io.OutputStream
import java.io.PipedInputStream
import java.io.PipedOutputStream
import javax.inject.Inject import javax.inject.Inject
private const val TAG = "ZimFileReader" private const val TAG = "ZimFileReader"
@ -80,7 +81,7 @@ class ZimFileReader constructor(
val description: String get() = jniKiwixReader.description val description: String get() = jniKiwixReader.description
val favicon: String? get() = jniKiwixReader.favicon val favicon: String? get() = jniKiwixReader.favicon
val language: String get() = jniKiwixReader.language val language: String get() = jniKiwixReader.language
val tags: String get() = "${getContent(Uri.parse("M/Tags"))}" val tags: String get() = "${getContent("M/Tags")}"
private val mediaCount: Int? private val mediaCount: Int?
get() = try { get() = try {
jniKiwixReader.mediaCount jniKiwixReader.mediaCount
@ -112,8 +113,9 @@ class ZimFileReader constructor(
fun getRandomArticleUrl(): String? = fun getRandomArticleUrl(): String? =
valueOfJniStringAfter(jniKiwixReader::getRandomPage) valueOfJniStringAfter(jniKiwixReader::getRandomPage)
fun load(uri: Uri): ParcelFileDescriptor { fun load(uri: String): InputStream? {
if ("$uri".matches(VIDEO_REGEX)) { val extension = uri.substringAfterLast(".")
if (videoExtensions.any { it == extension }) {
try { try {
return loadVideo(uri) return loadVideo(uri)
} catch (ioException: IOException) { } catch (ioException: IOException) {
@ -123,7 +125,7 @@ class ZimFileReader constructor(
return loadContent(uri) return loadContent(uri)
} }
fun readMimeType(uri: Uri) = "$uri".removeArguments().let { fun readMimeType(uri: String) = uri.removeArguments().let {
it.mimeType?.takeIf(String::isNotEmpty) ?: mimeTypeFromReader(it) it.mimeType?.takeIf(String::isNotEmpty) ?: mimeTypeFromReader(it)
}.also { Log.d(TAG, "getting mimetype for $uri = $it") } }.also { Log.d(TAG, "getting mimetype for $uri = $it") }
@ -134,78 +136,73 @@ class ZimFileReader constructor(
fun getRedirect(url: String) = "${toRedirect(url)}" fun getRedirect(url: String) = "${toRedirect(url)}"
fun isRedirect(url: String) = fun isRedirect(url: String) =
url.startsWith("$CONTENT_URI") && url != getRedirect(url) url.startsWith(CONTENT_PREFIX) && url != getRedirect(url)
private fun toRedirect(url: String) = private fun toRedirect(url: String) =
"$CONTENT_URI${jniKiwixReader.checkUrl(url.toUri().filePath)}".toUri() "$CONTENT_PREFIX${jniKiwixReader.checkUrl(url.toUri().filePath)}".toUri()
private fun loadContent(uri: Uri) = private fun loadContent(uri: String) =
try { try {
ParcelFileDescriptor.createPipe().also { val outputStream = PipedOutputStream()
streamZimContentToPipe(uri, AutoCloseOutputStream(it[1])) PipedInputStream(outputStream).also { streamZimContentToPipe(uri, outputStream) }
}[0]
} catch (ioException: IOException) { } catch (ioException: IOException) {
throw IOException("Could not open pipe for $uri", ioException) throw IOException("Could not open pipe for $uri", ioException)
} }
private fun loadVideo(uri: Uri): ParcelFileDescriptor { private fun loadVideo(uri: String): InputStream? {
val infoPair = jniKiwixReader.getDirectAccessInformation(uri.filePath) val infoPair = jniKiwixReader.getDirectAccessInformation(uri.filePath)
if (infoPair == null || !File(infoPair.filename).exists()) { if (infoPair == null || !File(infoPair.filename).exists()) {
return loadVideoFromCache(uri) return loadVideoFromCache(uri)
} }
return dup(infoPair.fileDescriptor) return AssetFileDescriptor(
infoPair.parcelFileDescriptor,
infoPair.offset,
jniKiwixReader.getArticleSize(uri.filePath)
).createInputStream()
} }
@Throws(IOException::class) @Throws(IOException::class)
private fun loadVideoFromCache(uri: Uri): ParcelFileDescriptor { private fun loadVideoFromCache(uri: String): FileInputStream {
val outputFile = File( return File(
FileUtils.getFileCacheDir(CoreApp.getInstance()), FileUtils.getFileCacheDir(CoreApp.getInstance()),
"$uri".substringAfterLast("/") uri.substringAfterLast("/")
) ).apply { writeBytes(getContent(uri)) }
FileOutputStream(outputFile).use { it.write(getContent(uri)) } .inputStream()
return ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_READ_ONLY)
} }
private fun streamZimContentToPipe( private fun getContent(url: String) = getContentAndMimeType(url).let { (content, _) -> content }
uri: Uri,
outputStream: AutoCloseOutputStream private fun streamZimContentToPipe(uri: String, outputStream: OutputStream) {
) { Completable.fromAction {
Single.just(Unit) try {
.subscribeOn(Schedulers.io()) outputStream.use {
.observeOn(Schedulers.io()) getContentAndMimeType(uri).let { (content: ByteArray, mimeType: String) ->
.subscribe( if ("text/css" == mimeType && nightModeConfig.isNightModeActive()) {
{ it.write(INVERT_IMAGES_VIDEO.toByteArray(Charsets.UTF_8))
try {
outputStream.use {
val mime = JNIKiwixString()
val size = JNIKiwixInt()
val url = JNIKiwixString(uri.filePath.removeArguments())
val content = getContent(url = url, mime = mime, size = size)
if ("text/css" == mime.value && nightModeConfig.isNightModeActive()) {
it.write(INVERT_IMAGES_VIDEO.toByteArray(Charsets.UTF_8))
}
it.write(content)
Log.d(
TAG,
"reading ${url.value}(mime: ${mime.value}, size: ${size.value}) finished."
)
} }
} catch (ioException: IOException) { it.write(content)
Log.e(TAG, "error writing pipe for $uri", ioException)
} }
}, }
Throwable::printStackTrace } catch (ioException: IOException) {
) Log.e(TAG, "error writing pipe for $uri", ioException)
}
}
.subscribeOn(Schedulers.io())
.subscribe({ }, Throwable::printStackTrace)
} }
private fun getContent(uri: Uri) = getContent(JNIKiwixString(uri.filePath.removeArguments())) private fun getContentAndMimeType(uri: String) = with(JNIKiwixString()) {
getContent(url = JNIKiwixString(uri.filePath.removeArguments()), mime = this) to value
}
private fun getContent( private fun getContent(
url: JNIKiwixString = JNIKiwixString(), url: JNIKiwixString = JNIKiwixString(),
jniKiwixString: JNIKiwixString = JNIKiwixString(), jniKiwixString: JNIKiwixString = JNIKiwixString(),
mime: JNIKiwixString = JNIKiwixString(), mime: JNIKiwixString = JNIKiwixString(),
size: JNIKiwixInt = JNIKiwixInt() size: JNIKiwixInt = JNIKiwixInt()
) = jniKiwixReader.getContent(url, jniKiwixString, mime, size) ) = jniKiwixReader.getContent(url, jniKiwixString, mime, size).also {
Log.d(TAG, "reading ${url.value}(mime: ${mime.value}, size: ${size.value}) finished.")
}
private fun valueOfJniStringAfter(jniStringFunction: (JNIKiwixString) -> Boolean) = private fun valueOfJniStringAfter(jniStringFunction: (JNIKiwixString) -> Boolean) =
JNIKiwixString().takeIf { jniStringFunction(it) }?.value JNIKiwixString().takeIf { jniStringFunction(it) }?.value
@ -233,9 +230,11 @@ class ZimFileReader constructor(
*/ */
@JvmField @JvmField
val UI_URI: Uri? = Uri.parse("content://org.kiwix.ui/") val UI_URI: Uri? = Uri.parse("content://org.kiwix.ui/")
@JvmField @JvmField
val CONTENT_URI: Uri? = val CONTENT_PREFIX =
Uri.parse("content://${CoreApp.getInstance().packageName}.zim.base/") Uri.parse("content://${CoreApp.getInstance().packageName}.zim.base/").toString()
private val INVERT_IMAGES_VIDEO = private val INVERT_IMAGES_VIDEO =
""" """
img, video, div[poster], div#header { img, video, div[poster], div#header {
@ -251,18 +250,18 @@ class ZimFileReader constructor(
filter: invert(0); filter: invert(0);
} }
""".trimIndent() """.trimIndent()
private val VIDEO_REGEX = Regex("([^\\s]+(\\.(?i)(3gp|mp4|m4a|webm|mkv|ogg|ogv))\$)") private val videoExtensions = listOf("3gp", "mp4", "m4a", "webm", "mkv", "ogg", "ogv")
} }
} }
private fun String.removeArguments() = substringBefore("?") private fun String.removeArguments() = substringBefore("?")
private val Pair.fileDescriptor: FileDescriptor?
get() = RandomAccessFile(filename, "r").apply { seek(offset.toLong()) }.fd
private val Uri.filePath: String private val Uri.filePath: String
get() = toString().filePath get() = toString().filePath
private val String.filePath: String private val String.filePath: String
get() = substringAfter("$CONTENT_URI").substringBefore("#") get() = substringAfter(CONTENT_PREFIX).substringBefore("#")
private val String.mimeType: String? private val String.mimeType: String?
get() = MimeTypeMap.getSingleton().getMimeTypeFromExtension( get() = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
MimeTypeMap.getFileExtensionFromUrl(this) MimeTypeMap.getFileExtensionFromUrl(this)
) )
private val Pair.parcelFileDescriptor: ParcelFileDescriptor?
get() = ParcelFileDescriptor.open(File(filename), ParcelFileDescriptor.MODE_READ_ONLY)

View File

@ -17,7 +17,7 @@
*/ */
package org.kiwix.kiwixmobile.core.reader package org.kiwix.kiwixmobile.core.reader
import android.net.Uri import android.webkit.WebResourceResponse
import org.kiwix.kiwixlib.JNIKiwixSearcher import org.kiwix.kiwixlib.JNIKiwixSearcher
import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Factory import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Factory
import java.io.File import java.io.File
@ -48,10 +48,6 @@ class ZimReaderContainer @Inject constructor(
else null else null
} }
fun readMimeType(uri: Uri) = zimFileReader?.readMimeType(uri)
fun load(uri: Uri) = zimFileReader?.load(uri)
fun searchSuggestions(prefix: String, count: Int) = fun searchSuggestions(prefix: String, count: Int) =
zimFileReader?.searchSuggestions(prefix, count) ?: false zimFileReader?.searchSuggestions(prefix, count) ?: false
@ -67,8 +63,15 @@ class ZimReaderContainer @Inject constructor(
fun getNextResult() = jniKiwixSearcher?.nextResult?.let { SearchResult(it.title) } fun getNextResult() = jniKiwixSearcher?.nextResult?.let { SearchResult(it.title) }
fun isRedirect(url: String): Boolean = zimFileReader?.isRedirect(url) == true fun isRedirect(url: String): Boolean = zimFileReader?.isRedirect(url) == true
fun getRedirect(url: String): String = zimFileReader?.getRedirect(url) ?: "" fun getRedirect(url: String): String = zimFileReader?.getRedirect(url) ?: ""
fun load(url: String) =
WebResourceResponse(
zimFileReader?.readMimeType(url),
Charsets.UTF_8.name(),
zimFileReader?.load(url)
)
val zimFile get() = zimFileReader?.zimFile val zimFile get() = zimFileReader?.zimFile
val zimCanonicalPath get() = zimFileReader?.zimFile?.canonicalPath val zimCanonicalPath get() = zimFileReader?.zimFile?.canonicalPath
val zimFileTitle get() = zimFileReader?.title val zimFileTitle get() = zimFileReader?.title
val mainPage get() = zimFileReader?.mainPage val mainPage get() = zimFileReader?.mainPage

View File

@ -18,6 +18,7 @@
package org.kiwix.kiwixmobile.core package org.kiwix.kiwixmobile.core
import android.net.Uri
import io.mockk.clearAllMocks import io.mockk.clearAllMocks
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
@ -58,6 +59,8 @@ class StorageObserverTest {
setScheduler(Schedulers.trampoline()) setScheduler(Schedulers.trampoline())
mockkStatic(CoreApp::class) mockkStatic(CoreApp::class)
every { CoreApp.getInstance().packageName } returns "pkg" every { CoreApp.getInstance().packageName } returns "pkg"
mockkStatic(Uri::class)
every { Uri.parse(any()).toString() } returns "pkg"
zimFileReader = mockk() zimFileReader = mockk()
} }