diff --git a/.travis.yml b/.travis.yml index 364d59e0a..951352b02 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,8 @@ jdk: oraclejdk8 sudo: required +dist: trusty + env: global: # switch glibc to a memory conserving mode @@ -38,8 +40,6 @@ cache: android: components: - - tools - - platform-tools - build-tools-28.0.3 - android-28 - extra-android-m2repository @@ -63,10 +63,11 @@ after_success: - ./gradlew kiwixtestUploadKiwix after_failure: - - export LOG_DIR = ${TRAVIS_HOME}/build/kiwix/kiwix-android/app/build/outputs/reports/androidTests/connected/flavors/KIWIX/ - - lynx --dump ${LOG_DIR}com.android.builder.testing.ConnectedDevice.html - - lynx --dump ${LOG_DIR}com.android.builder.testing.html - - lynx --dump ${LOG_DIR}org.kiwix.kiwixmobile.tests.BasicTest.html; + - export REPORT_DIR=${TRAVIS_HOME}/build/kiwix/kiwix-android/app/build/outputs/reports/ + - export LOG_DIR=${REPORT_DIR}androidTests/connected/flavors/KIWIX/ + - lynx -dump ${LOG_DIR}index.html + - lynx -dump ${REPORT_DIR}lint-results-kiwixDebug.html + before_deploy: # - export APP_CHANGELOG=$(cat app/src/kiwix/play/release-notes/en-US/default.txt) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e1e594832..d388dc72f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,11 +46,13 @@ Our branching strategy is based on [this article](https://nvie.com/posts/a-succe + **master** a history of releases, once merged to from develop and tagged we create a release on the play store & GitHub releases. + **develop** the actively worked on next release of the app, what we branch off of while working on new features and what we merge into upon feature completion -+ **feature/** or feature/\/ any branch under this directory is an actively developed feature, feature branches culminate in a PR, are merged and deleted. Typically a feature branch is off of develop and into develop but in rare scenarios if there is an issue in production a branch may be made off master to fix this issue, this type of feature branch must be merged to develop and master before being deleted. ++ **feature/** or feature/\/ any branch under this directory is an actively developed feature, feature branches culminate in a PR, are merged and deleted. + Typically a feature branch is off of develop and into develop but in rare scenarios if there is an issue in production a branch may be made off master to fix this issue, this type of feature branch must be merged to develop and master before being deleted. +Branch names should be in the format **#\-kebab-case-title** -All branches should have distinct history and should be visually easy to follow, for this reason only preform merge commits when merging code either by PR or when synchronising. +All branches should have distinct history and should be visually easy to follow, for this reason only perform merge commits when merging code either by PR or when synchronising. -Rebasing should be avoided. +If you wish to rebase you should be following the [Golden Rule](https://www.atlassian.com/git/tutorials/merging-vs-rebasing#the-golden-rule-of-rebasing) and ahere to the advice in the heading [Aside: Rebase as cleanup is awesome in the coding lifecycle](https://www.atlassian.com/git/articles/git-team-workflows-merge-or-rebase). ### Building diff --git a/README.md b/README.md index 8c0e7ac71..5651c16a1 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ We utilize different build variants (flavours) to build various different versio - [Butterknife](https://jakewharton.github.io/butterknife/) - View "injection" library for Android - [Mockito](https://github.com/mockito/mockito) - Most popular Mocking framework for unit tests written in Java - [RxJava](https://github.com/ReactiveX/RxJava) - Reactive Extensions for the JVM – a library for composing asynchronous and event-based programs using observable sequences for the Java VM. -- [ObjectBox] (https://github.com/objectbox/objectbox-java) - Reactive NoSQL Databse to replace SquiDb -- [MockK] (https://github.com/mockk/mockk) - Kotlin mocking library that allows mocking of final classes by default. -- [JUnit5] (https://github.com/junit-team/junit5/) - The next generation of JUnit -- [AssertJ] (https://github.com/joel-costigliola/assertj-core) - Fluent assertions for test code +- [ObjectBox](https://github.com/objectbox/objectbox-java) - Reactive NoSQL Databse to replace SquiDb +- [MockK](https://github.com/mockk/mockk) - Kotlin mocking library that allows mocking of final classes by default. +- [JUnit5](https://github.com/junit-team/junit5/) - The next generation of JUnit +- [AssertJ](https://github.com/joel-costigliola/assertj-core) - Fluent assertions for test code ## Contributing diff --git a/app/build.gradle b/app/build.gradle index 5100ebd4f..662ea7073 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,9 +56,6 @@ dependencies { archs = file("../kiwixlib/src/main/jniLibs").list() } - // Storage Devices - implementation "eu.mhutti1.utils.storage:android-storage-devices:0.6.2" - // Android Support implementation "androidx.appcompat:appcompat:$appCompatVersion" implementation "com.google.android.material:material:$materialVersion" @@ -125,9 +122,8 @@ dependencies { implementation "io.reactivex.rxjava2:rxjava:$rxJavaVersion" // Leak canary - releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3' - androidTestImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3' - debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0-alpha-2' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" implementation "android.arch.lifecycle:extensions:1.1.1" @@ -233,8 +229,11 @@ android { //TODO stop ignoring ignore 'MissingTranslation', 'CheckResult', + 'LabelFor', 'DuplicateStrings', 'LogConditional' + warning 'UnknownNullness', + 'SelectableText' baseline file("lint-baseline.xml") } @@ -274,6 +273,9 @@ android { // Release Type release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release buildConfigField "String", "KIWIX_DOWNLOAD_URL", "\"http://mirror.download.kiwix.org/\"" buildConfigField "boolean", "KIWIX_ERROR_ACTIVITY", "true" @@ -408,15 +410,6 @@ android { javaMaxHeapSize "4g" } - /* - Add back once proguard is configured - buildTypes { - release { - minifyEnabled true - proguardFiles getDefaultProguardFile("proguard-android.txt") - } - } - */ androidExtensions { experimental = true } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 000000000..fbedbf4f3 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,37 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +######################## +# Kiwix specific rules # +######################## + +#keep everything in kiwixlib +-keep class org.kiwix.kiwixlib.** { *; } + +## SimpleXml +-dontwarn com.bea.xml.stream.** +-dontwarn org.simpleframework.xml.stream.** +-keep class org.simpleframework.xml.**{ *; } +-keepclassmembers,allowobfuscation class * { + @org.simpleframework.xml.* ; + @org.simpleframework.xml.* (...); +} diff --git a/app/src/androidTestKiwix/java/org/kiwix/kiwixmobile/language/LanguageActivityTest.java b/app/src/androidTestKiwix/java/org/kiwix/kiwixmobile/language/LanguageActivityTest.java index ee5d1e4a6..d87544fb0 100644 --- a/app/src/androidTestKiwix/java/org/kiwix/kiwixmobile/language/LanguageActivityTest.java +++ b/app/src/androidTestKiwix/java/org/kiwix/kiwixmobile/language/LanguageActivityTest.java @@ -26,21 +26,15 @@ 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.Ignore; import org.junit.Rule; import org.junit.Test; import org.kiwix.kiwixmobile.R; -import org.kiwix.kiwixmobile.intro.IntroActivity; +import org.kiwix.kiwixmobile.zim_manager.ZimManageActivity; -import static androidx.test.InstrumentationRegistry.getInstrumentation; import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu; import static androidx.test.espresso.action.ViewActions.click; import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; import static androidx.test.espresso.action.ViewActions.replaceText; -import static androidx.test.espresso.action.ViewActions.swipeLeft; -import static androidx.test.espresso.action.ViewActions.swipeRight; -import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.isChecked; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; @@ -52,12 +46,11 @@ import static org.hamcrest.Matchers.allOf; import static org.hamcrest.core.IsNull.notNullValue; import static org.kiwix.kiwixmobile.testutils.Matcher.childAtPosition; import static org.kiwix.kiwixmobile.testutils.TestUtils.TEST_PAUSE_MS; -import static org.kiwix.kiwixmobile.testutils.ViewActions.setChecked; public class LanguageActivityTest { @Rule - public BaristaRule activityTestRule = BaristaRule.create(IntroActivity.class); + public BaristaRule activityTestRule = BaristaRule.create(ZimManageActivity.class); @Rule public GrantPermissionRule readPermissionRule = GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE); @@ -72,46 +65,8 @@ public class LanguageActivityTest { } @Test - @Ignore("Broken in 2.5")//TODO: Fix in 3.0 public void testLanguageActivity() { - BaristaSleepInteractions.sleep(TEST_PAUSE_MS); - onView(withId(R.id.get_started)).perform(click()); - BaristaSleepInteractions.sleep(TEST_PAUSE_MS); - - // Open the Library - openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext()); - onView(withText("Get Content")).perform(click()); - BaristaSleepInteractions.sleep(TEST_PAUSE_MS); - - ViewInteraction viewPager = onView(allOf(withId(R.id.manageViewPager), - childAtPosition(allOf(withId(R.id.zim_manager_main_activity), - childAtPosition(withId(android.R.id.content), 0)), 1), - isDisplayed())); - - // Verify that the "Choose Language" and the "Search" buttons are present only in the "online" tab - onView(withContentDescription("Search")).check(matches(notNullValue())); - // Test that the language selection screen does not open if the "Choose language" button is clicked, while the data is being loaded - onView(withContentDescription("Choose a language")).check(matches(notNullValue())) - .perform(click()); - - viewPager.perform(swipeRight()); - onView(withContentDescription("Search")).check(doesNotExist()); - onView(withContentDescription("Choose a language")).check(doesNotExist()); - viewPager.perform(swipeLeft()); - viewPager.perform(swipeLeft()); - onView(withContentDescription("Search")).check(doesNotExist()); - onView(withContentDescription("Choose a language")).check(doesNotExist()); - - viewPager.perform(swipeRight()); - - // Verify that the library is still visible - onView(allOf(withText("Library"), childAtPosition(allOf(withId(R.id.toolbar), - childAtPosition(withId(R.id.toolbar_layout), 0)), 1), isDisplayed())); - - // Make sure that the zim list has been loaded - //IdlingRegistry.getInstance().register(LibraryFragment.IDLING_RESOURCE); - onView(allOf(isDisplayed(), withText("Selected languages:"))).check(matches(notNullValue())); - + onView(withText("Online")).perform(click()); // Open the Language Activity onView(withContentDescription("Choose a language")).perform(click()); @@ -143,18 +98,7 @@ public class LanguageActivityTest { 0), isDisplayed())); - // Get a reference to the checkbox associated with the top unselected language - checkBox2 = onView( - allOf(withId(R.id.item_language_checkbox), - childAtPosition( - childAtPosition( - withId(R.id.recycler_view), - 2), - 0), - isDisplayed())); - // Initialise the language checkbox - checkBox2.perform(setChecked(false)); onView(withContentDescription("Save languages")).perform(click()); @@ -164,8 +108,6 @@ public class LanguageActivityTest { onView(withId(R.id.search_src_text)).perform(replaceText(language2), closeSoftKeyboard()); BaristaSleepInteractions.sleep(TEST_PAUSE_MS); - // Initialise the language checkbox - checkBox2.perform(setChecked(false)); onView(withContentDescription("Clear query")).perform(click()); // Collapse the search view to go to the full list of languages onView(withContentDescription("Collapse")).perform(click()); @@ -178,7 +120,7 @@ public class LanguageActivityTest { onView(withId(R.id.search_src_text)).perform(replaceText(language1), closeSoftKeyboard()); BaristaSleepInteractions.sleep(TEST_PAUSE_MS); - checkBox2.perform(click()); + checkBox1.perform(click()); onView(withContentDescription("Clear query")).perform(click()); // Collapse the search view to go to the full list of languages onView(withContentDescription("Collapse")).perform(click()); @@ -187,7 +129,7 @@ public class LanguageActivityTest { onView(withId(R.id.search_src_text)).perform(replaceText(language2), closeSoftKeyboard()); BaristaSleepInteractions.sleep(TEST_PAUSE_MS); - checkBox2.perform(click()); + checkBox1.perform(click()); onView(withContentDescription("Clear query")).perform(click()); onView(withContentDescription("Collapse")).perform(click()); @@ -216,7 +158,7 @@ public class LanguageActivityTest { onView(withId(R.id.search_src_text)).perform(replaceText(language1), closeSoftKeyboard()); BaristaSleepInteractions.sleep(TEST_PAUSE_MS); - checkBox2.check(matches(not(isChecked()))); + checkBox1.check(matches(not(isChecked()))); onView(withContentDescription("Clear query")).perform(click()); onView(withContentDescription("Collapse")).perform(click()); @@ -224,7 +166,7 @@ public class LanguageActivityTest { onView(withId(R.id.search_src_text)).perform(replaceText(language2), closeSoftKeyboard()); BaristaSleepInteractions.sleep(TEST_PAUSE_MS); - checkBox2.check(matches(not(isChecked()))); + checkBox1.check(matches(not(isChecked()))); onView(withContentDescription("Clear query")).perform(click()); onView(withContentDescription("Collapse")).perform(click()); onView(withContentDescription("Navigate up")).perform(click()); @@ -237,7 +179,7 @@ public class LanguageActivityTest { onView(withId(R.id.search_src_text)).perform(replaceText(language1), closeSoftKeyboard()); BaristaSleepInteractions.sleep(TEST_PAUSE_MS); - checkBox2.perform(click()); + checkBox1.perform(click()); onView(withContentDescription("Clear query")).perform(click()); onView(withContentDescription("Collapse")).perform(click()); @@ -245,7 +187,7 @@ public class LanguageActivityTest { onView(withId(R.id.search_src_text)).perform(replaceText(language2), closeSoftKeyboard()); BaristaSleepInteractions.sleep(TEST_PAUSE_MS); - checkBox2.perform(click()); + checkBox1.perform(click()); onView(withContentDescription("Clear query")).perform(click()); onView(withContentDescription("Collapse")).perform(click()); onView(withContentDescription("Save languages")).perform(click()); diff --git a/app/src/androidTestKiwix/java/org/kiwix/kiwixmobile/tests/SettingsTest.java b/app/src/androidTestKiwix/java/org/kiwix/kiwixmobile/tests/SettingsTest.java index b9c4b2e28..0c3596e2d 100644 --- a/app/src/androidTestKiwix/java/org/kiwix/kiwixmobile/tests/SettingsTest.java +++ b/app/src/androidTestKiwix/java/org/kiwix/kiwixmobile/tests/SettingsTest.java @@ -49,10 +49,6 @@ public class SettingsTest { withKey("pref_hidetoolbar"))) .perform(click()); - onData(allOf( - is(instanceOf(Preference.class)), - withKey("pref_bottomtoolbar"))) - .perform(click()); onData(allOf( is(instanceOf(Preference.class)), diff --git a/app/src/main/java/eu/mhutti1/utils/storage/ExternalPaths.java b/app/src/main/java/eu/mhutti1/utils/storage/ExternalPaths.java new file mode 100644 index 000000000..d6994a4fc --- /dev/null +++ b/app/src/main/java/eu/mhutti1/utils/storage/ExternalPaths.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016 Isaac Hutt + * + * 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 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +package eu.mhutti1.utils.storage; + +import android.annotation.SuppressLint; + +class ExternalPaths { + + @SuppressLint("SdCardPath") private static final String[] paths = { + "/storage/sdcard0", + "/storage/sdcard1", + "/storage/extsdcard", + "/storage/extSdCard", + "/storage/sdcard0/external_sdcard", + "/mnt/sdcard/external_sd", + "/mnt/external_sd", + "/mnt/media_rw/*", + "/removable/microsd", + "/mnt/emmc", + "/storage/external_SD", + "/storage/ext_sd", + "/storage/removable/sdcard1", + "/data/sdext", + "/data/sdext2", + "/data/sdext3", + "/data/sdext2", + "/data/sdext3", + "/data/sdext4", + "/sdcard", + "/sdcard1", + "/sdcard2", + "/storage/microsd", + "/mnt/extsd", + "/extsd", + "/mnt/sdcard", + "/misc/android", + }; + + public static String[] getPossiblePaths() { + return paths; + } +} diff --git a/app/src/main/java/eu/mhutti1/utils/storage/StorageDevice.java b/app/src/main/java/eu/mhutti1/utils/storage/StorageDevice.java new file mode 100644 index 000000000..0aa65af62 --- /dev/null +++ b/app/src/main/java/eu/mhutti1/utils/storage/StorageDevice.java @@ -0,0 +1,164 @@ +/* + * Copyright 2016 Isaac Hutt + * + * 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 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +package eu.mhutti1.utils.storage; + +import android.os.Build; +import android.os.StatFs; +import android.util.Log; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.text.DecimalFormat; + +public class StorageDevice { + + // File object containing device path + private final File mFile; + + private final boolean mInternal; + + private boolean mDuplicate = true; + + public StorageDevice(String path, boolean internal) { + mFile = new File(path); + mInternal = internal; + if (mFile.exists()) { + createLocationCode(); + } + } + + public StorageDevice(File file, boolean internal) { + mFile = file; + mInternal = internal; + if (mFile.exists()) { + createLocationCode(); + } + } + + // Get device path + public String getName() { + return mFile.getPath(); + } + + // Get available space on device + public String getSize() { + return bytesToHuman(getAvailableBytes()); + } + + private Long getAvailableBytes() { + StatFs statFs = new StatFs(mFile.getPath()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + return statFs.getBlockSizeLong() * statFs.getAvailableBlocksLong(); + } else { + return (long) statFs.getBlockSize() * (long) statFs.getAvailableBlocks(); + } + } + + public String getTotalSize() { + return bytesToHuman(getTotalBytes()); + } + + // Get total space on device + private Long getTotalBytes() { + StatFs statFs = new StatFs((mFile.getPath())); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + return statFs.getBlockSizeLong() * statFs.getBlockCountLong(); + } else { + return (long) statFs.getBlockSize() * (long) statFs.getBlockCount(); + } + } + + // Convert bytes to human readable form + private static String bytesToHuman(long size) { + long Kb = 1 * 1024; + long Mb = Kb * 1024; + long Gb = Mb * 1024; + long Tb = Gb * 1024; + long Pb = Tb * 1024; + long Eb = Pb * 1024; + + if (size < Kb) return floatForm(size) + " byte"; + if (size >= Kb && size < Mb) return floatForm((double) size / Kb) + " KB"; + if (size >= Mb && size < Gb) return floatForm((double) size / Mb) + " MB"; + if (size >= Gb && size < Tb) return floatForm((double) size / Gb) + " GB"; + if (size >= Tb && size < Pb) return floatForm((double) size / Tb) + " TB"; + if (size >= Pb && size < Eb) return floatForm((double) size / Pb) + " PB"; + if (size >= Eb) return floatForm((double) size / Eb) + " EB"; + + return "???"; + } + + public boolean isInternal() { + return mInternal; + } + + public File getPath() { + return mFile; + } + + private static String floatForm(double d) { + return new DecimalFormat("#.#").format(d); + } + + // Create unique file to identify duplicate devices. + private void createLocationCode() { + if (!getLocationCodeFromFolder(mFile)) { + File locationCode = new File(mFile.getPath(), ".storageLocationMarker"); + try { + locationCode.createNewFile(); + FileWriter fw = new FileWriter(locationCode); + fw.write(mFile.getPath()); + fw.close(); + } catch (IOException e) { + Log.d("android-storage-devices", "Unable to create marker file, duplicates may be listed"); + } + } + } + + // Check if there is already a device code in our path + private boolean getLocationCodeFromFolder(File folder) { + File locationCode = new File(folder.getPath(), ".storageLocationMarker"); + if (locationCode.exists()) { + try ( BufferedReader br = new BufferedReader(new FileReader(locationCode))){ + if (br.readLine().equals(mFile.getPath())) { + mDuplicate = false; + } else { + mDuplicate = true; + return true; + } + } catch (Exception e) { + return true; + } + } + String path = folder.getPath(); + String parent = path.substring(0, path.lastIndexOf("/")); + if (parent.equals("")) { + mDuplicate = false; + return false; + } + return getLocationCodeFromFolder(new File(parent)); + } + + public boolean isDuplicate() { + return mDuplicate; + } +} diff --git a/app/src/main/java/eu/mhutti1/utils/storage/StorageDeviceUtils.java b/app/src/main/java/eu/mhutti1/utils/storage/StorageDeviceUtils.java new file mode 100644 index 000000000..68a79d4b8 --- /dev/null +++ b/app/src/main/java/eu/mhutti1/utils/storage/StorageDeviceUtils.java @@ -0,0 +1,131 @@ +/* + * Copyright 2016 Isaac Hutt + * + * 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 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +package eu.mhutti1.utils.storage; + +import android.content.Context; +import android.os.Environment; +import androidx.core.content.ContextCompat; +import java.io.File; +import java.io.RandomAccessFile; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.util.ArrayList; + +public class StorageDeviceUtils { + + public static ArrayList getStorageDevices(Context context, boolean writable) { + ArrayList storageDevices = new ArrayList<>(); + + // Add as many possible mount points as we know about + + // Only add this device if its very likely that we have missed a users sd card + if (Environment.isExternalStorageEmulated()) { + // This is our internal storage directory + storageDevices.add(new StorageDevice( + generalisePath(Environment.getExternalStorageDirectory().getPath(), writable), true)); + } else { + // This is an external storage directory + storageDevices.add(new StorageDevice( + generalisePath(Environment.getExternalStorageDirectory().getPath(), writable), false)); + } + + // These are possible manufacturer sdcard mount points + + String[] paths = ExternalPaths.getPossiblePaths(); + + for (String path : paths) { + if (path.endsWith("*")) { + File root = new File(path.substring(0, path.length() - 1)); + File[] directories = root.listFiles(file -> file.isDirectory()); + if (directories != null) { + for (File dir : directories) { + storageDevices.add(new StorageDevice(dir, false)); + } + } + } else { + storageDevices.add(new StorageDevice(path, false)); + } + } + + // Iterate through any sdcards manufacturers may have specified + for (File file : ContextCompat.getExternalFilesDirs(context, "")) { + if (file != null) { + storageDevices.add(new StorageDevice(generalisePath(file.getPath(), writable), false)); + } + } + + // Check all devices exist, we can write to them if required and they are not duplicates + return checkStorageValid(writable, storageDevices); + } + + // Remove app specific path from directories so that we can search them from the top + private static String generalisePath(String path, boolean writable) { + if (writable) { + return path; + } + int endIndex = path.lastIndexOf("/Android/data/"); + if (endIndex != -1) { + return path.substring(0, endIndex); + } + return path; + } + + private static ArrayList checkStorageValid(boolean writable, + ArrayList storageDevices) { + ArrayList activeDevices = new ArrayList<>(); + ArrayList devicePaths = new ArrayList<>(); + for (StorageDevice device : storageDevices) { + if (existsAndIsDirAndWritableIfRequiredAndNotDuplicate(writable, devicePaths, device)) { + activeDevices.add(device); + devicePaths.add(device); + } + } + return activeDevices; + } + + private static boolean existsAndIsDirAndWritableIfRequiredAndNotDuplicate(boolean writable, + ArrayList devicePaths, StorageDevice device) { + final File devicePath = device.getPath(); + return devicePath.exists() + && devicePath.isDirectory() + && (canWrite(devicePath) || !writable) + && !device.isDuplicate() + && !devicePaths.contains(device); + } + + // Amazingly file.canWrite() does not always return the correct value + private static boolean canWrite(File file) { + final String filePath = file + "/test.txt"; + try { + RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw"); + FileChannel fileChannel = randomAccessFile.getChannel(); + FileLock fileLock = fileChannel.lock(); + fileLock.release(); + fileChannel.close(); + randomAccessFile.close(); + return true; + } catch (Exception ex) { + return false; + } finally { + new File(filePath).delete(); + } + } +} + diff --git a/app/src/main/java/eu/mhutti1/utils/storage/StorageSelectArrayAdapter.java b/app/src/main/java/eu/mhutti1/utils/storage/StorageSelectArrayAdapter.java new file mode 100644 index 000000000..394cff47c --- /dev/null +++ b/app/src/main/java/eu/mhutti1/utils/storage/StorageSelectArrayAdapter.java @@ -0,0 +1,72 @@ +/* + * Copyright 2016 Isaac Hutt + * + * 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 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +package eu.mhutti1.utils.storage; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; +import java.util.ArrayList; +import org.kiwix.kiwixmobile.R; + +class StorageSelectArrayAdapter extends ArrayAdapter { + + private final String mInternal; + + private final String mExternal; + + public StorageSelectArrayAdapter(Context context, int resource, ArrayList devices, + String internal, String external) { + super(context, resource, devices); + mInternal = internal; + mExternal = external; + } + + @SuppressLint("SetTextI18n") @Override + public View getView(int position, View convertView, ViewGroup parent) { + + ViewHolder holder; + if (convertView == null) { + convertView = View.inflate(getContext(), R.layout.device_item, null); + holder = new ViewHolder(); + holder.fileName = convertView.findViewById(R.id.file_name); + holder.fileSize = convertView.findViewById(R.id.file_size); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + StorageDevice device = getItem(position); + if (device.isInternal()) { + holder.fileName.setText(mInternal); + } else { + holder.fileName.setText(mExternal); + } + holder.fileSize.setText(device.getSize() + " / " + device.getTotalSize()); + + return convertView; + } + + class ViewHolder { + TextView fileName; + TextView fileSize; + } +} diff --git a/app/src/main/java/eu/mhutti1/utils/storage/StorageSelectDialog.java b/app/src/main/java/eu/mhutti1/utils/storage/StorageSelectDialog.java new file mode 100644 index 000000000..d4302d8e8 --- /dev/null +++ b/app/src/main/java/eu/mhutti1/utils/storage/StorageSelectDialog.java @@ -0,0 +1,114 @@ +/* + * Copyright 2016 Isaac Hutt + * + * 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 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +package eu.mhutti1.utils.storage; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.TextView; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; +import java.io.File; +import org.kiwix.kiwixmobile.R; + +public class StorageSelectDialog extends DialogFragment implements ListView.OnItemClickListener { + + // Activities/Fragments can create instances of a StorageSelectDialog and bind a listener to get its result + + public static final String STORAGE_DIALOG_THEME = "THEME"; + + public static final String STORAGE_DIALOG_INTERNAL = "INTERNAL"; + + public static final String STORAGE_DIALOG_EXTERNAL = "EXTERNAL"; + + private StorageSelectArrayAdapter mAdapter; + + private OnSelectListener mOnSelectListener; + private String mTitle; + + private String mInternal = "Internal"; + + private String mExternal = "External"; + + @Override + public void onCreate(Bundle savedInstanceState) { + if (getArguments() != null) { + // Set string values + mInternal = getArguments().getString(STORAGE_DIALOG_INTERNAL, mInternal); + mExternal = getArguments().getString(STORAGE_DIALOG_EXTERNAL, mExternal); + // Set the theme to a supplied value + if (getArguments().containsKey(STORAGE_DIALOG_THEME)) { + setStyle(DialogFragment.STYLE_NORMAL, getArguments().getInt(STORAGE_DIALOG_THEME)); + } + } + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.storage_select_dialog, container, false); + TextView title = rootView.findViewById(R.id.title); + title.setText(mTitle); + ListView listView = rootView.findViewById(R.id.device_list); + mAdapter = new StorageSelectArrayAdapter(getActivity(), 0, + StorageDeviceUtils.getStorageDevices(getActivity(), true), mInternal, mExternal); + listView.setAdapter(mAdapter); + listView.setOnItemClickListener(this); + Button button = rootView.findViewById(R.id.button); + final EditText editText = rootView.findViewById(R.id.editText); + button.setOnClickListener(view -> { + if (editText.getText().length() != 0) { + String path = editText.getText().toString(); + if (new File(path).exists()) { + mAdapter.add(new StorageDevice(path, false)); + } + } + }); + return rootView; + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + if (mOnSelectListener != null) { + mOnSelectListener.selectionCallback(mAdapter.getItem(position)); + } + dismiss(); + } + + public void setOnSelectListener(OnSelectListener selectListener) { + mOnSelectListener = selectListener; + } + + public interface OnSelectListener { + void selectionCallback(StorageDevice s); + } + + @Override + public void show(FragmentManager fm, String text) { + mTitle = text; + super.show(fm, text); + } +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/KiwixApplication.java b/app/src/main/java/org/kiwix/kiwixmobile/KiwixApplication.java index 1f96ddc21..5311cc96f 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/KiwixApplication.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/KiwixApplication.java @@ -26,7 +26,6 @@ import android.util.Log; import androidx.appcompat.app.AppCompatDelegate; import androidx.multidex.MultiDexApplication; import com.jakewharton.threetenabp.AndroidThreeTen; -import com.squareup.leakcanary.LeakCanary; import dagger.android.AndroidInjector; import dagger.android.DispatchingAndroidInjector; import dagger.android.HasActivityInjector; @@ -73,11 +72,6 @@ public class KiwixApplication extends MultiDexApplication implements HasActivity @Override public void onCreate() { super.onCreate(); - if (LeakCanary.isInAnalyzerProcess(this)) { - // This process is dedicated to LeakCanary for heap analysis. - // You should not init your app in this process. - return; - } AndroidThreeTen.init(this); if (isExternalStorageWritable()) { File appDirectory = new File(Environment.getExternalStorageDirectory() + "/Kiwix"); @@ -110,7 +104,6 @@ public class KiwixApplication extends MultiDexApplication implements HasActivity Log.d("KIWIX", "Started KiwixApplication"); applicationComponent.inject(this); - LeakCanary.install(this); if (BuildConfig.DEBUG) { StrictMode.setThreadPolicy(buildThreadPolicy(new StrictMode.ThreadPolicy.Builder())); StrictMode.setVmPolicy(buildVmPolicy(new StrictMode.VmPolicy.Builder())); diff --git a/app/src/main/java/org/kiwix/kiwixmobile/bookmark/BookmarksActivity.java b/app/src/main/java/org/kiwix/kiwixmobile/bookmark/BookmarksActivity.java index 48c23f828..48a84074f 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/bookmark/BookmarksActivity.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/bookmark/BookmarksActivity.java @@ -92,7 +92,7 @@ public class BookmarksActivity extends BaseActivity implements BookmarksContract protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); presenter.attachView(this); - setContentView(R.layout.activity_bookmarks_history_language); + setContentView(R.layout.activity_bookmarks); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); diff --git a/app/src/main/java/org/kiwix/kiwixmobile/database/newdb/entities/LanguageEntity.kt b/app/src/main/java/org/kiwix/kiwixmobile/database/newdb/entities/LanguageEntity.kt index 8b5896191..1dba26e86 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/database/newdb/entities/LanguageEntity.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/database/newdb/entities/LanguageEntity.kt @@ -24,7 +24,7 @@ data class LanguageEntity( ) fun toLanguageModel() = - Language(locale, active, occurencesOfLanguage) + Language(locale, active, occurencesOfLanguage, id) } class StringToLocaleConverter : PropertyConverter { diff --git a/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ActivityBindingModule.java b/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ActivityBindingModule.java index cc97109fb..ee09602b3 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ActivityBindingModule.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ActivityBindingModule.java @@ -12,7 +12,6 @@ import org.kiwix.kiwixmobile.history.HistoryModule; import org.kiwix.kiwixmobile.intro.IntroActivity; import org.kiwix.kiwixmobile.intro.IntroModule; import org.kiwix.kiwixmobile.language.LanguageActivity; -import org.kiwix.kiwixmobile.language.LanguageModule; import org.kiwix.kiwixmobile.main.MainActivity; import org.kiwix.kiwixmobile.main.MainModule; import org.kiwix.kiwixmobile.search.SearchActivity; @@ -61,7 +60,7 @@ public abstract class ActivityBindingModule { public abstract SplashActivity provideSplashActivity(); @PerActivity - @ContributesAndroidInjector(modules = LanguageModule.class) + @ContributesAndroidInjector public abstract LanguageActivity provideLanguageActivity(); @PerActivity diff --git a/app/src/main/java/org/kiwix/kiwixmobile/di/modules/NetworkModule.java b/app/src/main/java/org/kiwix/kiwixmobile/di/modules/NetworkModule.java index f0c8b4ce6..1a86ded46 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/di/modules/NetworkModule.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/di/modules/NetworkModule.java @@ -36,7 +36,7 @@ import org.kiwix.kiwixmobile.data.remote.UserAgentInterceptor; @Provides @Singleton OkHttpClient provideOkHttpClient() { HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); - logging.setLevel(BuildConfig.DEBUG ? Level.BODY : Level.BASIC); + logging.setLevel(BuildConfig.DEBUG ? Level.BASIC : Level.NONE); return new OkHttpClient().newBuilder().followRedirects(true).followSslRedirects(true) .connectTimeout(10, TimeUnit.SECONDS) diff --git a/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ViewModelModule.kt b/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ViewModelModule.kt index 45c3db03e..f69993da8 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ViewModelModule.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/di/modules/ViewModelModule.kt @@ -7,6 +7,7 @@ import dagger.Module import dagger.multibindings.IntoMap import org.kiwix.kiwixmobile.KiwixViewModelFactory import org.kiwix.kiwixmobile.di.ViewModelKey +import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel /* @@ -31,7 +32,12 @@ abstract class ViewModelModule { @Binds @IntoMap @ViewModelKey(ZimManageViewModel::class) - internal abstract fun bindUserViewModel(userViewModel: ZimManageViewModel): ViewModel + internal abstract fun bindZimManageViewModel(zimManageViewModel: ZimManageViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(LanguageViewModel::class) + internal abstract fun bindLanguageViewModel(languageViewModel: LanguageViewModel): ViewModel @Binds internal abstract fun bindViewModelFactory(factory: KiwixViewModelFactory): ViewModelProvider.Factory diff --git a/app/src/main/java/org/kiwix/kiwixmobile/downloader/DownloadFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/downloader/DownloadFragment.kt index 77d58af4c..3c4fd2bba 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/downloader/DownloadFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/downloader/DownloadFragment.kt @@ -23,7 +23,6 @@ import android.view.View import android.view.ViewGroup import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.layout_download_management.download_management_no_downloads @@ -31,6 +30,7 @@ import kotlinx.android.synthetic.main.layout_download_management.zim_downloader_ import org.kiwix.kiwixmobile.base.BaseFragment import org.kiwix.kiwixmobile.di.components.ActivityComponent import org.kiwix.kiwixmobile.downloader.model.DownloadItem +import org.kiwix.kiwixmobile.extensions.viewModel import org.kiwix.kiwixmobile.utils.DialogShower import org.kiwix.kiwixmobile.utils.KiwixDialog.YesNoDialog.StopDownload import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil @@ -44,10 +44,10 @@ class DownloadFragment : BaseFragment() { @Inject lateinit var sharedPreferenceUtil: SharedPreferenceUtil @Inject lateinit var downloader: Downloader - private val zimManageViewModel: ZimManageViewModel by lazy { - ViewModelProviders.of(activity!!, viewModelFactory) - .get(ZimManageViewModel::class.java) + private val zimManageViewModel by lazy { + activity!!.viewModel(viewModelFactory) } + private val downloadAdapter = DownloadAdapter { dialogShower.show(StopDownload, { downloader.cancelDownload(it) }) } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/extensions/ActivityExtensions.kt b/app/src/main/java/org/kiwix/kiwixmobile/extensions/ActivityExtensions.kt index 19f27325b..9beb1c147 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/extensions/ActivityExtensions.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/extensions/ActivityExtensions.kt @@ -1,10 +1,16 @@ package org.kiwix.kiwixmobile.extensions import android.app.Activity +import android.content.Intent import android.view.ActionMode import android.view.ActionMode.Callback import android.view.Menu import android.view.MenuItem +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProviders +import org.kiwix.kiwixmobile.base.BaseActivity fun Activity.startActionMode( menuId: Int, @@ -41,3 +47,12 @@ fun Activity.startActionMode( }) } + +inline fun Activity.start() { + startActivity(Intent(this, T::class.java)) +} + +inline fun FragmentActivity.viewModel(viewModelFactory: ViewModelProvider.Factory) = + ViewModelProviders.of(this, viewModelFactory) + .get(T::class.java) + diff --git a/app/src/main/java/org/kiwix/kiwixmobile/history/HistoryActivity.java b/app/src/main/java/org/kiwix/kiwixmobile/history/HistoryActivity.java index ee375d784..75e02369d 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/history/HistoryActivity.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/history/HistoryActivity.java @@ -106,7 +106,7 @@ public class HistoryActivity extends BaseActivity implements HistoryContract.Vie protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); presenter.attachView(this); - setContentView(R.layout.activity_bookmarks_history_language); + setContentView(R.layout.activity_history); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageActivity.java b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageActivity.java deleted file mode 100644 index af3a11b15..000000000 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageActivity.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.kiwix.kiwixmobile.language; - -import android.content.Intent; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuItem; -import android.widget.Toast; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.widget.SearchView; -import androidx.appcompat.widget.Toolbar; -import androidx.recyclerview.widget.RecyclerView; -import butterknife.BindView; -import java.util.ArrayList; -import java.util.List; -import javax.inject.Inject; -import org.kiwix.kiwixmobile.R; -import org.kiwix.kiwixmobile.base.BaseActivity; -import org.kiwix.kiwixmobile.zim_manager.Language; - -public class LanguageActivity extends BaseActivity implements LanguageContract.View { - - public static final String LANGUAGE_LIST = "languages"; - private final ArrayList languages = new ArrayList<>(); - private final ArrayList allLanguages = new ArrayList<>(); - - @BindView(R.id.toolbar) - Toolbar toolbar; - @BindView(R.id.recycler_view) - RecyclerView recyclerView; - @Inject - LanguageContract.Presenter presenter; - - private LanguageAdapter languageAdapter; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - presenter.attachView(this); - setContentView(R.layout.activity_bookmarks_history_language); - setSupportActionBar(toolbar); - - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setHomeAsUpIndicator(R.drawable.ic_clear_white_24dp); - actionBar.setTitle(R.string.select_languages); - } - - languages.addAll(getIntent().getParcelableArrayListExtra(LANGUAGE_LIST)); - allLanguages.addAll(languages); - languageAdapter = new LanguageAdapter(languages); - recyclerView.setAdapter(languageAdapter); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.menu_language, menu); - MenuItem search = menu.findItem(R.id.menu_language_search); - ((SearchView) search.getActionView()).setOnQueryTextListener( - new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - return false; - } - - @Override - public boolean onQueryTextChange(String newText) { - languages.clear(); - languages.addAll(allLanguages); - presenter.filerLanguages(languages, newText); - return true; - } - }); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - onBackPressed(); - return true; - case R.id.menu_language_save: - languages.clear(); - languages.addAll(allLanguages); - presenter.saveLanguages(languages); - - Toast.makeText(this, getString(R.string.languages_saved), Toast.LENGTH_SHORT).show(); - Intent intent = new Intent(); - intent.putParcelableArrayListExtra(LANGUAGE_LIST, languages); - setResult(RESULT_OK, intent); - finish(); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - protected void onDestroy() { - presenter.detachView(); - super.onDestroy(); - } - - @Override - public void notifyLanguagesFiltered(List languages) { - this.languages.clear(); - this.languages.addAll(languages); - languageAdapter.categorizeLanguages(); - languageAdapter.notifyDataSetChanged(); - } -} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageActivity.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageActivity.kt new file mode 100644 index 000000000..43ebcce67 --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageActivity.kt @@ -0,0 +1,110 @@ +package org.kiwix.kiwixmobile.language + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.widget.SearchView +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.reactivex.disposables.CompositeDisposable +import kotlinx.android.synthetic.main.activity_language.language_progressbar +import kotlinx.android.synthetic.main.activity_language.recycler_view +import kotlinx.android.synthetic.main.activity_language.toolbar +import org.kiwix.kiwixmobile.R +import org.kiwix.kiwixmobile.base.BaseActivity +import org.kiwix.kiwixmobile.extensions.viewModel +import org.kiwix.kiwixmobile.language.viewmodel.Action.Select +import org.kiwix.kiwixmobile.language.viewmodel.State.Content +import org.kiwix.kiwixmobile.language.viewmodel.State.Loading +import org.kiwix.kiwixmobile.language.viewmodel.State.Saving +import org.kiwix.kiwixmobile.language.adapter.LanguageAdapter +import org.kiwix.kiwixmobile.language.adapter.LanguageDelegate.HeaderDelegate +import org.kiwix.kiwixmobile.language.adapter.LanguageDelegate.LanguageItemDelegate +import org.kiwix.kiwixmobile.language.viewmodel.Action +import org.kiwix.kiwixmobile.language.viewmodel.LanguageViewModel +import org.kiwix.kiwixmobile.language.viewmodel.State +import org.kiwix.kiwixmobile.zim_manager.SimpleTextListener +import javax.inject.Inject + +class LanguageActivity : BaseActivity() { + + private val languageViewModel by lazy { viewModel(viewModelFactory) } + + @Inject lateinit var viewModelFactory: ViewModelProvider.Factory + + private val compositeDisposable = CompositeDisposable() + + private val languageAdapter = + LanguageAdapter( + LanguageItemDelegate { languageViewModel.actions.offer(Select(it)) }, + HeaderDelegate() + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_language) + setSupportActionBar(toolbar) + + supportActionBar?.let { + it.setDisplayHomeAsUpEnabled(true) + it.setHomeAsUpIndicator(R.drawable.ic_clear_white_24dp) + it.setTitle(R.string.select_languages) + } + recycler_view.run { + adapter = languageAdapter + layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) + setHasFixedSize(true) + } + languageViewModel.state.observe(this, Observer { + render(it) + }) + compositeDisposable.add( + languageViewModel.effects.subscribe( + { + it.invokeWith(this) + }, + Throwable::printStackTrace + ) + ) + } + + override fun onDestroy() { + super.onDestroy() + compositeDisposable.clear() + } + + private fun render(state: State) = when (state) { + Loading -> language_progressbar.show() + is Content -> { + language_progressbar.hide() + languageAdapter.items = state.viewItems + } + Saving -> Unit + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_language, menu) + val search = menu.findItem(R.id.menu_language_search) + (search.actionView as SearchView).setOnQueryTextListener(SimpleTextListener { + languageViewModel.actions.offer(Action.Filter(it)) + }) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + R.id.menu_language_save -> { + languageViewModel.actions.offer(Action.SaveAll) + return true + } + } + return super.onOptionsItemSelected(item) + } + +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageAdapter.java b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageAdapter.java deleted file mode 100644 index 0192e2e59..000000000 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageAdapter.java +++ /dev/null @@ -1,139 +0,0 @@ -package org.kiwix.kiwixmobile.language; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import butterknife.BindView; -import butterknife.ButterKnife; -import java.util.ArrayList; -import org.kiwix.kiwixmobile.R; -import org.kiwix.kiwixmobile.zim_manager.Language; - -class LanguageAdapter extends RecyclerView.Adapter { - private static final int TYPE_HEADER = 0; - private static final int TYPE_ITEM = 1; - private final ArrayList languages; - private final ArrayList selectedLanguages = new ArrayList<>(); - private final ArrayList unselectedLanguages = new ArrayList<>(); - - LanguageAdapter(ArrayList languages) { - this.languages = languages; - categorizeLanguages(); - } - - void categorizeLanguages() { - selectedLanguages.clear(); - unselectedLanguages.clear(); - //for (Language language : languages) { - // if (language.active != null && language.active.equals(true)) { - // selectedLanguages.add(language); - // } else { - // language.active = false; - // unselectedLanguages.add(language); - // } - //} - //Collections.sort(selectedLanguages); - //Collections.sort(unselectedLanguages); - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - if (viewType == TYPE_ITEM) { - View view = - LayoutInflater.from(parent.getContext()).inflate(R.layout.item_language, parent, false); - return new ViewHolder(view); - } - return new Header( - LayoutInflater.from(parent.getContext()).inflate(R.layout.header_date, parent, false)); - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder item, int position) { - if (item instanceof Header) { - Header header = (Header) item; - if (position == 0) { - header.header.setText(R.string.your_languages); - } else { - header.header.setText(R.string.other_languages); - } - return; - } - Language language; - if (position - 1 < selectedLanguages.size()) { - language = selectedLanguages.get(position - 1); - } else { - language = unselectedLanguages.get(position - selectedLanguages.size() - 2); - } - ViewHolder holder = (ViewHolder) item; - //holder.languageName.setText(language.language); - //holder.languageLocalizedName.setText(language.languageLocalized); - //holder.booksCount.setText(holder.booksCount.getContext().getResources() - // .getQuantityString(R.plurals.books_count, language.booksCount, language.booksCount)); - //if (language.active == null) { - // language.active = false; - //} - //holder.checkBox.setChecked(language.active); - //View.OnClickListener onClickListener = v -> { - // language.active = holder.checkBox.isChecked(); - // if (language.active) { - // unselectedLanguages.remove(language); - // selectedLanguages.add(language); - // } else { - // unselectedLanguages.add(language); - // selectedLanguages.remove(language); - // } - // Collections.sort(selectedLanguages); - // Collections.sort(unselectedLanguages); - // notifyDataSetChanged(); - //}; - //holder.itemView.setOnClickListener(v -> { - // holder.checkBox.toggle(); - // onClickListener.onClick(v); - //}); - //holder.checkBox.setOnClickListener(onClickListener); - } - - @Override - public int getItemViewType(int position) { - if (position == 0 || position == selectedLanguages.size() + 1) { - return TYPE_HEADER; - } - return TYPE_ITEM; - } - - @Override - public int getItemCount() { - return selectedLanguages.size() + unselectedLanguages.size() + 2; - } - - class ViewHolder extends RecyclerView.ViewHolder { - @BindView(R.id.item_language_name) - TextView languageName; - @BindView(R.id.item_language_localized_name) - TextView languageLocalizedName; - @BindView(R.id.item_language_books_count) - TextView booksCount; - @BindView(R.id.item_language_checkbox) - CheckBox checkBox; - - ViewHolder(View itemView) { - super(itemView); - ButterKnife.bind(this, itemView); - } - } - - class Header extends RecyclerView.ViewHolder { - @BindView(R.id.header_date) - TextView header; - - Header(View itemView) { - super(itemView); - ButterKnife.bind(this, itemView); - } - } -} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageContract.java b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageContract.java deleted file mode 100644 index ee6035570..000000000 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageContract.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.kiwix.kiwixmobile.language; - -import java.util.List; -import org.kiwix.kiwixmobile.base.BaseContract; -import org.kiwix.kiwixmobile.zim_manager.Language; - -interface LanguageContract { - interface View extends BaseContract.View { - void notifyLanguagesFiltered(List languages); - } - - interface Presenter extends BaseContract.Presenter { - void filerLanguages(List languages, String query); - - void saveLanguages(List languages); - } -} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageModule.java b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageModule.java deleted file mode 100644 index 11dec7227..000000000 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageModule.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.kiwix.kiwixmobile.language; - -import dagger.Module; -import dagger.Provides; -import org.kiwix.kiwixmobile.di.PerActivity; - -@Module -public class LanguageModule { - @PerActivity - @Provides - LanguageContract.Presenter provideLanguagePresenter(LanguagePresenter presenter) { - return presenter; - } -} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguagePresenter.java b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguagePresenter.java deleted file mode 100644 index 42052b70d..000000000 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguagePresenter.java +++ /dev/null @@ -1,76 +0,0 @@ -package org.kiwix.kiwixmobile.language; - -import android.util.Log; -import io.reactivex.CompletableObserver; -import io.reactivex.Scheduler; -import io.reactivex.disposables.Disposable; -import java.util.List; -import javax.inject.Inject; -import org.kiwix.kiwixmobile.base.BasePresenter; -import org.kiwix.kiwixmobile.data.DataSource; -import org.kiwix.kiwixmobile.di.PerActivity; -import org.kiwix.kiwixmobile.di.qualifiers.Computation; -import org.kiwix.kiwixmobile.di.qualifiers.MainThread; -import org.kiwix.kiwixmobile.zim_manager.Language; - -@PerActivity -class LanguagePresenter extends BasePresenter - implements LanguageContract.Presenter { - private final Scheduler mainThread; - private final Scheduler computation; - private final DataSource dataSource; - - @Inject LanguagePresenter(DataSource dataSource, @Computation Scheduler computation, - @MainThread Scheduler mainThread) { - this.computation = computation; - this.mainThread = mainThread; - this.dataSource = dataSource; - } - - @Override - public void filerLanguages(List languages, String query) { - //Observable.fromIterable(languages) - // .filter(language -> language.language.toLowerCase().contains(query.toLowerCase()) || - // language.languageLocalized.toLowerCase().contains(query.toLowerCase())) - // .toList() - // .subscribeOn(computation) - // .observeOn(mainThread) - // .subscribe(new SingleObserver>() { - // @Override - // public void onSubscribe(Disposable d) { - // compositeDisposable.add(d); - // } - // - // @Override - // public void onSuccess(List languages) { - // view.notifyLanguagesFiltered(languages); - // } - // - // @Override - // public void onError(Throwable e) { - // Log.e("LanguagePresenter", e.toString()); - // } - // }); - } - - @Override - public void saveLanguages(List languages) { - dataSource.saveLanguages(languages) - .subscribe(new CompletableObserver() { - @Override - public void onSubscribe(Disposable d) { - - } - - @Override - public void onComplete() { - - } - - @Override - public void onError(Throwable e) { - Log.e("LanguagePresenter", e.toString()); - } - }); - } -} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/adapter/LanguageAdapter.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/adapter/LanguageAdapter.kt new file mode 100644 index 000000000..5860bf524 --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/adapter/LanguageAdapter.kt @@ -0,0 +1,27 @@ +/* + * Kiwix Android + * Copyright (C) 2018 Kiwix + * + * 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 . + */ +package org.kiwix.kiwixmobile.language.adapter + +import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.base.AdapterDelegate +import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.base.BaseDelegateAdapter + +class LanguageAdapter( + vararg delegates: AdapterDelegate +) : BaseDelegateAdapter(*delegates) { + override fun getIdFor(item: LanguageListItem) = item.id +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/adapter/LanguageDelegate.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/adapter/LanguageDelegate.kt new file mode 100644 index 000000000..8997a0ab1 --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/adapter/LanguageDelegate.kt @@ -0,0 +1,30 @@ +package org.kiwix.kiwixmobile.language.adapter + +import android.view.ViewGroup +import org.kiwix.kiwixmobile.R +import org.kiwix.kiwixmobile.extensions.inflate +import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.HeaderItem +import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.LanguageItem +import org.kiwix.kiwixmobile.language.adapter.LanguageListViewHolder.HeaderViewHolder +import org.kiwix.kiwixmobile.language.adapter.LanguageListViewHolder.LanguageViewHolder +import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.base.AbsDelegateAdapter + +sealed class LanguageDelegate> : + AbsDelegateAdapter { + + class HeaderDelegate : LanguageDelegate() { + override val itemClass = HeaderItem::class.java + + override fun createViewHolder(parent: ViewGroup) = + HeaderViewHolder(parent.inflate(R.layout.header_date, false)) + + } + + class LanguageItemDelegate(private val clickAction: (LanguageItem) -> Unit) : LanguageDelegate() { + override val itemClass = LanguageItem::class.java + + override fun createViewHolder(parent: ViewGroup) = + LanguageViewHolder(parent.inflate(R.layout.item_language, false), clickAction) + + } +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/adapter/LanguageListItem.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/adapter/LanguageListItem.kt new file mode 100644 index 000000000..3450d467d --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/adapter/LanguageListItem.kt @@ -0,0 +1,22 @@ +package org.kiwix.kiwixmobile.language.adapter + +import org.kiwix.kiwixmobile.zim_manager.Language + +sealed class LanguageListItem { + abstract val id: Long + + data class HeaderItem constructor( + override val id: Long + ) : LanguageListItem() { + companion object { + const val SELECTED = Long.MAX_VALUE + const val OTHER = Long.MIN_VALUE + } + + } + + data class LanguageItem( + val language: Language, + override val id: Long = language.id + ) : LanguageListItem() +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/adapter/LanguageListViewHolder.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/adapter/LanguageListViewHolder.kt new file mode 100644 index 000000000..f8ac59493 --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/adapter/LanguageListViewHolder.kt @@ -0,0 +1,42 @@ +package org.kiwix.kiwixmobile.language.adapter + +import android.view.View +import kotlinx.android.synthetic.main.header_date.header_date +import kotlinx.android.synthetic.main.item_language.item_language_books_count +import kotlinx.android.synthetic.main.item_language.item_language_checkbox +import kotlinx.android.synthetic.main.item_language.item_language_clickable_area +import kotlinx.android.synthetic.main.item_language.item_language_localized_name +import kotlinx.android.synthetic.main.item_language.item_language_name +import org.kiwix.kiwixmobile.R +import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.HeaderItem +import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.LanguageItem +import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.base.BaseViewHolder + +sealed class LanguageListViewHolder(override val containerView: View) : + BaseViewHolder(containerView) { + class HeaderViewHolder(view: View) : LanguageListViewHolder(view) { + override fun bind(item: HeaderItem) { + header_date.setText( + if (item.id == HeaderItem.SELECTED) R.string.your_languages + else R.string.other_languages + ) + } + } + + class LanguageViewHolder( + view: View, + val clickAction: (LanguageItem) -> Unit + ) : LanguageListViewHolder(view) { + override fun bind(item: LanguageItem) { + val language = item.language + item_language_name.text = language.language + item_language_localized_name.text = language.languageLocalized + item_language_books_count.text = containerView.resources.getQuantityString( + R.plurals.books_count, language.occurencesOfLanguage, language.occurencesOfLanguage + ) + item_language_checkbox.isChecked = language.active + item_language_clickable_area.setOnClickListener { clickAction(item) } + } + + } +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/Action.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/Action.kt new file mode 100644 index 000000000..5a0804d99 --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/Action.kt @@ -0,0 +1,11 @@ +package org.kiwix.kiwixmobile.language.viewmodel + +import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.LanguageItem +import org.kiwix.kiwixmobile.zim_manager.Language + +sealed class Action { + data class UpdateLanguages(val languages: List) : Action() + data class Filter(val filter: String) : Action() + data class Select(val language: LanguageItem) : Action() + object SaveAll : Action() +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModel.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModel.kt new file mode 100644 index 000000000..1ade26a3a --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModel.kt @@ -0,0 +1,91 @@ +package org.kiwix.kiwixmobile.language.viewmodel + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.processors.PublishProcessor +import org.kiwix.kiwixmobile.database.newdb.dao.NewLanguagesDao +import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.LanguageItem +import org.kiwix.kiwixmobile.language.viewmodel.Action.Filter +import org.kiwix.kiwixmobile.language.viewmodel.Action.SaveAll +import org.kiwix.kiwixmobile.language.viewmodel.Action.Select +import org.kiwix.kiwixmobile.language.viewmodel.Action.UpdateLanguages +import org.kiwix.kiwixmobile.language.viewmodel.State.Content +import org.kiwix.kiwixmobile.language.viewmodel.State.Loading +import org.kiwix.kiwixmobile.language.viewmodel.State.Saving +import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.SideEffect +import javax.inject.Inject + +class LanguageViewModel @Inject constructor( + private val languageDao: NewLanguagesDao +) : ViewModel() { + + val state = MutableLiveData().apply { value = Loading } + val actions = PublishProcessor.create() + val effects = PublishProcessor.create>() + + private val compositeDisposable = CompositeDisposable() + + init { + compositeDisposable.addAll( + actions.map { reduce(it, state.value!!) } + .distinctUntilChanged() + .subscribe(state::postValue, Throwable::printStackTrace), + languageDao.languages().filter { it.isNotEmpty() } + .subscribe( + { + actions.offer(UpdateLanguages(it)) + }, + Throwable::printStackTrace + ) + ) + } + + private fun reduce( + action: Action, + currentState: State + ): State { + return when (action) { + is UpdateLanguages -> when (currentState) { + Loading -> Content(action.languages) + else -> currentState + } + is Filter -> { + when (currentState) { + is Content -> filterContent(action.filter, currentState) + else -> currentState + } + } + is Select -> + when (currentState) { + is Content -> updateSelection(action.language, currentState) + else -> currentState + } + SaveAll -> + when (currentState) { + is Content -> saveAll(currentState) + else -> currentState + } + } + } + + private fun saveAll(currentState: Content): State { + effects.offer( + SaveLanguagesAndFinish( + currentState.items, languageDao + ) + ) + return Saving + } + + private fun updateSelection( + languageItem: LanguageItem, + currentState: Content + ) = currentState.select(languageItem) + + private fun filterContent( + filter: String, + currentState: Content + ) = currentState.updateFilter(filter) + +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/SaveLanguagesAndFinish.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/SaveLanguagesAndFinish.kt new file mode 100644 index 000000000..82f6d8dc2 --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/SaveLanguagesAndFinish.kt @@ -0,0 +1,39 @@ +/* + * Kiwix Android + * Copyright (C) 2018 Kiwix + * + * 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 . + */ +package org.kiwix.kiwixmobile.language.viewmodel + +import android.app.Activity +import io.reactivex.Flowable +import io.reactivex.schedulers.Schedulers +import org.kiwix.kiwixmobile.database.newdb.dao.NewLanguagesDao +import org.kiwix.kiwixmobile.zim_manager.Language +import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.SideEffect + +data class SaveLanguagesAndFinish( + val languages: List, + val languageDao: NewLanguagesDao +) : SideEffect { + + override fun invokeWith(activity: Activity) { + Flowable.fromCallable { languageDao.insert(languages) } + .subscribeOn(Schedulers.io()) + .subscribe({ + activity.finish() + }, Throwable::printStackTrace) + } +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/State.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/State.kt new file mode 100644 index 000000000..02be4c9e0 --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/State.kt @@ -0,0 +1,63 @@ +package org.kiwix.kiwixmobile.language.viewmodel + +import org.kiwix.kiwixmobile.language.adapter.LanguageListItem +import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.HeaderItem +import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.LanguageItem +import org.kiwix.kiwixmobile.zim_manager.Language + +sealed class State { + object Loading : State() + object Saving : State() + data class Content( + val items: List, + val filter: String = "", + val viewItems: List = createViewList( + items, filter + ) + ) : State() { + fun select(languageItem: LanguageItem) = Content( + items.map { if (it.id == languageItem.id) it.copy(active = !it.active) else it }, + filter + ) + + fun updateFilter(filter: String) = Content(items, filter) + + companion object { + internal fun createViewList( + items: List, + filter: String + ) = activeItems( + items, filter + ) + otherItems(items, filter) + + private fun activeItems( + items: List, + filter: String + ) = + createLanguageSection( + items, filter, { it.active }, HeaderItem.SELECTED + ) + + private fun otherItems( + items: List, + filter: String + ) = + createLanguageSection( + items, filter, { !it.active }, HeaderItem.OTHER + ) + + private fun createLanguageSection( + items: List, + filter: String, + filterCondition: (Language) -> Boolean, + headerId: Long + ) = items.filter(filterCondition) + .filter { filter.isEmpty() or it.matches(filter) } + .takeIf { it.isNotEmpty() } + ?.let { listOf(HeaderItem(headerId)) + it.map { language -> LanguageItem(language) } } + ?: emptyList() + + } + } + +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/main/AddNoteDialog.java b/app/src/main/java/org/kiwix/kiwixmobile/main/AddNoteDialog.java index dfd9fde0c..e463ce37d 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/main/AddNoteDialog.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/main/AddNoteDialog.java @@ -39,7 +39,6 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; - import butterknife.BindView; import butterknife.ButterKnife; import butterknife.Unbinder; @@ -49,13 +48,14 @@ import static org.kiwix.kiwixmobile.utils.Constants.NOTES_DIRECTORY; /** * Created by @author Aditya-Sood (21/05/19) as a part of GSoC 2019 * - * AddNoteDialog extends DialogFragment and is used to display the note corresponding to a particular - * article (of a particular zim file/wiki/book) as a full-screen dialog fragment. + * AddNoteDialog extends DialogFragment and is used to display the note corresponding to a + * particular article (of a particular zim file/wiki/book) as a full-screen dialog fragment. * * Notes are saved as text files at location: "{External Storage}/Kiwix/Notes/ZimFileName/ArticleUrl.txt" - * */ + */ -public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDialogFragment.UserClickListener { +public class AddNoteDialog extends DialogFragment + implements ConfirmationAlertDialogFragment.UserClickListener { public static final String TAG = "AddNoteDialog"; @@ -70,36 +70,51 @@ public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDi private Unbinder unbinder; + private String zimFileName; private String zimFileTitle; private String articleTitle; - private String zimNoteDirectoryName; // Corresponds to "ZimFileName" of "{External Storage}/Kiwix/Notes/ZimFileName/ArticleUrl.txt" - private String articleNotefileName; // Corresponds to "ArticleUrl" of "{External Storage}/Kiwix/Notes/ZimFileName/ArticleUrl.txt" + // Corresponds to "ZimFileName" of "{External Storage}/Kiwix/Notes/ZimFileName/ArticleUrl.txt" + private String zimNoteDirectoryName; + // Corresponds to "ArticleUrl" of "{External Storage}/Kiwix/Notes/ZimFileName/ArticleUrl.txt" + private String articleNotefileName; private boolean noteFileExists = false; - private boolean noteEdited = false; // Keeps track of state of the note (whether edited since last save) + boolean noteEdited = false; // Keeps track of state of the note (whether edited since last save) - private String ZIM_NOTES_DIRECTORY; // Stores path to directory for the currently open zim's notes + private String ZIM_NOTES_DIRECTORY; // Stores path to directory for the currently open zim's notes - public AddNoteDialog(SharedPreferenceUtil sharedPreferenceUtil) { + public AddNoteDialog(@NonNull SharedPreferenceUtil sharedPreferenceUtil) { this.sharedPreferenceUtil = sharedPreferenceUtil; } @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setStyle(DialogFragment.STYLE_NORMAL, sharedPreferenceUtil.nightMode() ? R.style.AddNoteDialogStyle_Night : R.style.AddNoteDialogStyle); + setStyle(DialogFragment.STYLE_NORMAL, + sharedPreferenceUtil.nightMode() ? R.style.AddNoteDialogStyle_Night + : R.style.AddNoteDialogStyle); - zimFileTitle = ZimContentProvider.getZimFileTitle(); - articleTitle = ((MainActivity)getActivity()).getCurrentWebView().getTitle(); + // Returns name of the form ".../Kiwix/granbluefantasy_en_all_all_nopic_2018-10.zim" + zimFileName = ZimContentProvider.getZimFile(); - zimNoteDirectoryName = getZimNoteDirectoryName(); - articleNotefileName = getArticleNotefileName(); + if (zimFileName != null) { // No zim file currently opened + zimFileTitle = ZimContentProvider.getZimFileTitle(); + articleTitle = ((MainActivity) getActivity()).getCurrentWebView().getTitle(); - ZIM_NOTES_DIRECTORY = NOTES_DIRECTORY + zimNoteDirectoryName + "/"; + zimNoteDirectoryName = getZimNoteDirectoryName(); + articleNotefileName = getArticleNotefileName(); + + ZIM_NOTES_DIRECTORY = NOTES_DIRECTORY + zimNoteDirectoryName + "/"; + } else { + showToast(R.string.error_filenotfound, Toast.LENGTH_LONG); + closeKeyboard(); + getFragmentManager().beginTransaction().remove(AddNoteDialog.this).commit(); + } } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public @NonNull View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { super.onCreateView(inflater, container, savedInstanceState); View view = inflater.inflate(R.layout.dialog_add_note, container, false); unbinder = ButterKnife.bind(this, view); @@ -142,7 +157,8 @@ public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDi addNoteEditText.addTextChangedListener(new TextWatcher() { @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { @@ -152,26 +168,26 @@ public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDi } @Override - public void afterTextChanged(Editable s) {} + public void afterTextChanged(Editable s) { + } }); return view; } private @NonNull String getZimNoteDirectoryName() { - String zimFileName = ZimContentProvider.getZimFile(); // Returns name of the form ".../Kiwix/granbluefantasy_en_all_all_nopic_2018-10.zim" - String noteDirectoryName = getTextAfterLastSlashWithoutExtension(zimFileName); - return (!noteDirectoryName.isEmpty()) ? noteDirectoryName : zimFileTitle; // Incase the required ZIM file name couldn't be extracted + return (!noteDirectoryName.isEmpty()) ? noteDirectoryName : zimFileTitle; } private @NonNull String getArticleNotefileName() { - String articleUrl = ((MainActivity) getActivity()).getCurrentWebView().getUrl(); // Returns url of the form: "content://org.kiwix.kiwixmobile.zim.base/A/Main_Page.html" + // Returns url of the form: "content://org.kiwix.kiwixmobile.zim.base/A/Main_Page.html" + String articleUrl = ((MainActivity) getActivity()).getCurrentWebView().getUrl(); String notefileName = getTextAfterLastSlashWithoutExtension(articleUrl); - return (!notefileName.isEmpty()) ? notefileName : articleTitle; // Incase the required html file name couldn't be extracted + return (!notefileName.isEmpty()) ? notefileName : articleTitle; } private @NonNull String getTextAfterLastSlashWithoutExtension(@NonNull String path) { @@ -185,8 +201,8 @@ public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDi int rightmostSlash = path.lastIndexOf('/'); int rightmostDot = path.lastIndexOf('.'); - if(rightmostSlash > -1 && rightmostDot > -1) { - return (path.substring(rightmostSlash+1, rightmostDot)); + if (rightmostSlash > -1 && rightmostDot > -1) { + return (path.substring(rightmostSlash + 1, rightmostDot)); } return ""; // If couldn't find the dot and/or slash @@ -204,16 +220,18 @@ public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDi }; } - private void exitAddNoteDialog() { - if(noteEdited) { - Fragment previousInstance = getActivity().getSupportFragmentManager().findFragmentByTag(ConfirmationAlertDialogFragment.TAG); + void exitAddNoteDialog() { + if (noteEdited) { + Fragment previousInstance = getActivity().getSupportFragmentManager() + .findFragmentByTag(ConfirmationAlertDialogFragment.TAG); - if(previousInstance == null) { + if (previousInstance == null) { // Custom AlertDialog for taking user confirmation before closing note dialog in case of unsaved changes - DialogFragment newFragment = new ConfirmationAlertDialogFragment(sharedPreferenceUtil, TAG, R.string.confirmation_alert_dialog_message); - newFragment.show(getActivity().getSupportFragmentManager(), ConfirmationAlertDialogFragment.TAG); + DialogFragment newFragment = new ConfirmationAlertDialogFragment(sharedPreferenceUtil, TAG, + R.string.confirmation_alert_dialog_message); + newFragment.show(getActivity().getSupportFragmentManager(), + ConfirmationAlertDialogFragment.TAG); } - } else { // Closing unedited note dialog straightaway dismissAddNoteDialog(); @@ -221,36 +239,33 @@ public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDi } private void disableMenuItems() { - if(toolbar.getMenu() != null) { + if (toolbar.getMenu() != null) { MenuItem saveItem = toolbar.getMenu().findItem(R.id.save_note); MenuItem shareItem = toolbar.getMenu().findItem(R.id.share_note); saveItem.setEnabled(false); shareItem.setEnabled(false); saveItem.getIcon().setAlpha(130); shareItem.getIcon().setAlpha(130); - } else { Log.d(TAG, "Toolbar without inflated menu"); } } - private void enableSaveNoteMenuItem() { - if(toolbar.getMenu() != null) { + void enableSaveNoteMenuItem() { + if (toolbar.getMenu() != null) { MenuItem saveItem = toolbar.getMenu().findItem(R.id.save_note); saveItem.setEnabled(true); saveItem.getIcon().setAlpha(255); - } else { Log.d(TAG, "Toolbar without inflated menu"); } } - private void enableShareNoteMenuItem() { - if(toolbar.getMenu() != null) { + void enableShareNoteMenuItem() { + if (toolbar.getMenu() != null) { MenuItem shareItem = toolbar.getMenu().findItem(R.id.share_note); shareItem.setEnabled(true); shareItem.getIcon().setAlpha(255); - } else { Log.d(TAG, "Toolbar without inflated menu"); } @@ -260,32 +275,35 @@ public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDi public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - if(!noteFileExists) { + if (!noteFileExists) { // Prepare for input in case of empty/new note addNoteEditText.requestFocus(); showKeyboard(); } } - public void showKeyboard(){ - InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + private void showKeyboard() { + InputMethodManager inputMethodManager = + (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0); } - public void closeKeyboard(){ - InputMethodManager inputMethodManager = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + void closeKeyboard() { + InputMethodManager inputMethodManager = + (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); inputMethodManager.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0); } - private void saveNote(String noteText) { + void saveNote(String noteText) { /* String content of the EditText, given by noteText, is saved into the text file given by: * "{External Storage}/Kiwix/Notes/ZimFileTitle/ArticleTitle.txt" * */ - if(isExternalStorageWritable()) { + if (isExternalStorageWritable()) { - if(ContextCompat.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + if (ContextCompat.checkSelfPermission(getContext(), + Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { Log.d(TAG, "WRITE_EXTERNAL_STORAGE permission not granted"); showToast(R.string.note_save_unsuccessful, Toast.LENGTH_LONG); return; @@ -294,12 +312,12 @@ public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDi File notesFolder = new File(ZIM_NOTES_DIRECTORY); boolean folderExists = true; - if(!notesFolder.exists()) { + if (!notesFolder.exists()) { // Try creating folder if it doesn't exist folderExists = notesFolder.mkdirs(); } - if(folderExists) { + if (folderExists) { File noteFile = new File(notesFolder.getAbsolutePath(), articleNotefileName + ".txt"); // Save note text-file code: @@ -309,21 +327,17 @@ public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDi fileOutputStream.close(); showToast(R.string.note_save_successful, Toast.LENGTH_SHORT); noteEdited = false; // As no unsaved changes remain - } catch (IOException e) { e.printStackTrace(); showToast(R.string.note_save_unsuccessful, Toast.LENGTH_LONG); } - } else { showToast(R.string.note_save_unsuccessful, Toast.LENGTH_LONG); Log.d(TAG, "Required folder doesn't exist"); } - } - else { + } else { showToast(R.string.note_save_error_storage_not_writable, Toast.LENGTH_LONG); } - } private void displayNote() { @@ -335,30 +349,20 @@ public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDi File noteFile = new File(ZIM_NOTES_DIRECTORY + articleNotefileName + ".txt"); - if(noteFile.exists()) { + if (noteFile.exists()) { noteFileExists = true; StringBuilder contents = new StringBuilder(); - try { + try (BufferedReader input = new BufferedReader(new java.io.FileReader(noteFile))) { + String line = null; - BufferedReader input = new BufferedReader(new java.io.FileReader(noteFile)); - try { - String line = null; - - while((line = input.readLine()) != null) { - contents.append(line); - contents.append(System.getProperty("line.separator")); - } - } catch (IOException e) { - e.printStackTrace(); - Log.d(TAG, "Error reading line with BufferedReader"); - } finally { - input.close(); + while ((line = input.readLine()) != null) { + contents.append(line); + contents.append(System.getProperty("line.separator")); } - } catch (IOException e) { e.printStackTrace(); - Log.d(TAG, "Error closing BufferedReader"); + Log.d(TAG, "Error reading line with BufferedReader"); } addNoteEditText.setText(contents.toString()); // Display the note content @@ -369,49 +373,51 @@ public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDi // No action in case the note file for the currently open article doesn't exist } - private void shareNote() { + void shareNote() { /* The note text file corresponding to the currently open article, given at: * "{External Storage}/Kiwix/Notes/ZimFileTitle/ArticleTitle.txt" * is shared via an app-chooser intent * */ - if(noteEdited) { - saveNote(addNoteEditText.getText().toString()); // Save edited note before sharing the text file + if (noteEdited) { + saveNote( + addNoteEditText.getText().toString()); // Save edited note before sharing the text file } File noteFile = new File(ZIM_NOTES_DIRECTORY + articleNotefileName + ".txt"); Uri noteFileUri = null; - if(noteFile.exists()) { + if (noteFile.exists()) { if (Build.VERSION.SDK_INT >= 24) { // From Nougat 7 (API 24) access to files is shared temporarily with other apps // Need to use FileProvider for the same - noteFileUri = FileProvider.getUriForFile(getContext(), BuildConfig.APPLICATION_ID+".fileprovider", noteFile); - + noteFileUri = + FileProvider.getUriForFile(getContext(), BuildConfig.APPLICATION_ID + ".fileprovider", + noteFile); } else { noteFileUri = Uri.fromFile(noteFile); } - } else { showToast(R.string.note_share_error_file_missing, Toast.LENGTH_SHORT); } - if(noteFileUri != null) { + if (noteFileUri != null) { Intent noteFileShareIntent = new Intent(Intent.ACTION_SEND); noteFileShareIntent.setType("application/octet-stream"); noteFileShareIntent.putExtra(Intent.EXTRA_STREAM, noteFileUri); noteFileShareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - Intent shareChooser = Intent.createChooser(noteFileShareIntent, getString(R.string.note_share_app_chooser_title)); + Intent shareChooser = Intent.createChooser(noteFileShareIntent, + getString(R.string.note_share_app_chooser_title)); - if(noteFileShareIntent.resolveActivity(getActivity().getPackageManager()) != null) { + if (noteFileShareIntent.resolveActivity(getActivity().getPackageManager()) != null) { startActivity(shareChooser); } } } - public static boolean isExternalStorageWritable() { + static boolean isExternalStorageWritable() { return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()); } @@ -440,7 +446,7 @@ public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDi super.onStart(); Dialog dialog = getDialog(); - if(dialog != null) { + if (dialog != null) { int width = ViewGroup.LayoutParams.MATCH_PARENT; int height = ViewGroup.LayoutParams.MATCH_PARENT; dialog.getWindow().setLayout(width, height); @@ -454,5 +460,4 @@ public class AddNoteDialog extends DialogFragment implements ConfirmationAlertDi unbinder.unbind(); } } - } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java b/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java index 7f3de6b2a..41bb39f86 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/main/MainActivity.java @@ -20,6 +20,7 @@ package org.kiwix.kiwixmobile.main; import android.Manifest; +import android.annotation.SuppressLint; import android.app.Activity; import android.appwidget.AppWidgetManager; import android.content.ActivityNotFoundException; @@ -154,7 +155,7 @@ import static org.kiwix.kiwixmobile.utils.StyleUtils.dialogStyle; import static org.kiwix.kiwixmobile.utils.UpdateUtils.reformatProviderUrl; public class MainActivity extends BaseActivity implements WebViewCallback, - MainContract.View{ + MainContract.View { private static final String NEW_TAB = "NEW_TAB"; private static final String HOME_URL = "file:///android_asset/home.html"; @@ -276,7 +277,6 @@ public class MainActivity extends BaseActivity implements WebViewCallback, } }; - private static void updateWidgets(Context context) { Intent intent = new Intent(context.getApplicationContext(), KiwixSearchWidget.class); intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); @@ -321,6 +321,7 @@ public class MainActivity extends BaseActivity implements WebViewCallback, } } + @SuppressLint("ClickableViewAccessibility") @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -333,6 +334,15 @@ public class MainActivity extends BaseActivity implements WebViewCallback, setSupportActionBar(toolbar); actionBar = getSupportActionBar(); + toolbar.setOnTouchListener(new OnSwipeTouchListener(this) { + + @Override + @SuppressLint("SyntheticAccessor") + public void onSwipeBottom() { + showTabSwitcher(); + } + }); + tableDrawerRight = tableDrawerRightContainer.getHeaderView(0).findViewById(R.id.right_drawer_list); @@ -359,13 +369,13 @@ public class MainActivity extends BaseActivity implements WebViewCallback, wasHideToolbar = isHideToolbar; booksAdapter = new BooksOnDiskAdapter( - new BookOnDiskDelegate.BookDelegate(sharedPreferenceUtil, - bookOnDiskItem -> { - open(bookOnDiskItem); - return Unit.INSTANCE; - }, - null, - null), + new BookOnDiskDelegate.BookDelegate(sharedPreferenceUtil, + bookOnDiskItem -> { + open(bookOnDiskItem); + return Unit.INSTANCE; + }, + null, + null), BookOnDiskDelegate.LanguageDelegate.INSTANCE ); @@ -846,14 +856,14 @@ public class MainActivity extends BaseActivity implements WebViewCallback, break; case R.id.menu_add_note: - if(requestExternalStorageWritePermissionForNotes()) { + if (requestExternalStorageWritePermissionForNotes()) { // Check permission since notes are stored in the public-external storage showAddNoteDialog(); } break; case R.id.menu_clear_notes: - if(requestExternalStorageWritePermissionForNotes()) { // Check permission since notes are stored in the public-external storage + if (requestExternalStorageWritePermissionForNotes()) { // Check permission since notes are stored in the public-external storage showClearAllNotesDialog(); } break; @@ -939,12 +949,13 @@ public class MainActivity extends BaseActivity implements WebViewCallback, } /** Method to delete all user notes */ - private void clearAllNotes() { + void clearAllNotes() { boolean result = true; // Result of all delete() calls is &&-ed to this variable - if(AddNoteDialog.isExternalStorageWritable()) { - if(ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + if (AddNoteDialog.isExternalStorageWritable()) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + != PackageManager.PERMISSION_GRANTED) { Log.d("MainActivity", "WRITE_EXTERNAL_STORAGE permission not granted"); showToast(R.string.ext_storage_permission_not_granted, Toast.LENGTH_LONG); return; @@ -955,17 +966,17 @@ public class MainActivity extends BaseActivity implements WebViewCallback, File notesDirectory = new File(NOTES_DIRECTORY); File[] filesInNotesDirectory = notesDirectory.listFiles(); - if(filesInNotesDirectory == null) { // Notes folder doesn't exist + if (filesInNotesDirectory == null) { // Notes folder doesn't exist showToast(R.string.notes_deletion_none_found, Toast.LENGTH_LONG); return; } - for(File wikiFileDirectory : filesInNotesDirectory) { - if(wikiFileDirectory.isDirectory()) { + for (File wikiFileDirectory : filesInNotesDirectory) { + if (wikiFileDirectory.isDirectory()) { File[] filesInWikiDirectory = wikiFileDirectory.listFiles(); - for(File noteFile : filesInWikiDirectory) { - if(noteFile.isFile()) { + for (File noteFile : filesInWikiDirectory) { + if (noteFile.isFile()) { result = result && noteFile.delete(); } } @@ -974,10 +985,11 @@ public class MainActivity extends BaseActivity implements WebViewCallback, result = result && wikiFileDirectory.delete(); // Wiki specific notes directory deleted } - result = result && notesDirectory.delete(); // "{External Storage}/Kiwix/Notes" directory deleted + result = + result && notesDirectory.delete(); // "{External Storage}/Kiwix/Notes" directory deleted } - if(result) { + if (result) { showToast(R.string.notes_deletion_successful, Toast.LENGTH_SHORT); } else { showToast(R.string.notes_deletion_unsuccessful, Toast.LENGTH_SHORT); @@ -990,23 +1002,24 @@ public class MainActivity extends BaseActivity implements WebViewCallback, Fragment previousInstance = getSupportFragmentManager().findFragmentByTag(AddNoteDialog.TAG); // To prevent multiple instances of the DialogFragment - if(previousInstance == null) { + if (previousInstance == null) { /* Since the DialogFragment is never added to the back-stack, so findFragmentByTag() - * returning null means that the AddNoteDialog is currently not on display (as doesn't exist) - **/ + * returning null means that the AddNoteDialog is currently not on display (as doesn't exist) + **/ AddNoteDialog dialogFragment = new AddNoteDialog(sharedPreferenceUtil); - dialogFragment.show(fragmentTransaction, AddNoteDialog.TAG); // For DialogFragments, show() handles the fragment commit and display + dialogFragment.show(fragmentTransaction, AddNoteDialog.TAG); + // For DialogFragments, show() handles the fragment commit and display } } private boolean requestExternalStorageWritePermissionForNotes() { - if(Build.VERSION.SDK_INT >= 23) { // For Marshmallow & higher API levels + if (Build.VERSION.SDK_INT >= 23) { // For Marshmallow & higher API levels - if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED) { return true; - } else { - if(shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { /* shouldShowRequestPermissionRationale() returns false when: * 1) User has previously checked on "Don't ask me again", and/or * 2) Permission has been disabled on device @@ -1014,9 +1027,9 @@ public class MainActivity extends BaseActivity implements WebViewCallback, showToast(R.string.ext_storage_permission_rationale_add_note, Toast.LENGTH_LONG); } - requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_WRITE_STORAGE_PERMISSION_ADD_NOTE); + requestPermissions(new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, + REQUEST_WRITE_STORAGE_PERMISSION_ADD_NOTE); } - } else { // For Android versions below Marshmallow 6.0 (API 23) return true; // As already requested at install time } @@ -1202,12 +1215,12 @@ public class MainActivity extends BaseActivity implements WebViewCallback, case REQUEST_WRITE_STORAGE_PERMISSION_ADD_NOTE: { - if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Successfully granted permission, so opening the note keeper showAddNoteDialog(); - } else { - Toast.makeText(getApplicationContext(), getString(R.string.ext_storage_write_permission_denied_add_note), Toast.LENGTH_LONG); + Toast.makeText(getApplicationContext(), + getString(R.string.ext_storage_write_permission_denied_add_note), Toast.LENGTH_LONG); } break; @@ -1327,7 +1340,8 @@ public class MainActivity extends BaseActivity implements WebViewCallback, //Check maybe need refresh String articleUrl = getCurrentWebView().getUrl(); boolean isBookmark = false; - BookmarkItem bookmark = BookmarkItem.fromZimContentProvider(getCurrentWebView().getTitle(),articleUrl); + BookmarkItem bookmark = + BookmarkItem.fromZimContentProvider(getCurrentWebView().getTitle(), articleUrl); if (articleUrl != null && !bookmarks.contains(articleUrl)) { if (ZimContentProvider.getId() != null) { presenter.saveBookmark(bookmark); @@ -1442,12 +1456,11 @@ public class MainActivity extends BaseActivity implements WebViewCallback, private void updateBottomToolbarVisibility() { if (checkNull(bottomToolbar)) { - if (sharedPreferenceUtil.getPrefBottomToolbar() && !HOME_URL.equals( + if (!HOME_URL.equals( getCurrentWebView().getUrl()) && tabSwitcherRoot.getVisibility() != View.VISIBLE) { bottomToolbar.setVisibility(View.VISIBLE); - if (getCurrentWebView() instanceof ToolbarStaticKiwixWebView - && sharedPreferenceUtil.getPrefBottomToolbar()) { + if (getCurrentWebView() instanceof ToolbarStaticKiwixWebView) { contentFrame.setPadding(0, 0, 0, (int) getResources().getDimension(R.dimen.bottom_toolbar_height)); } else { @@ -1979,7 +1992,8 @@ public class MainActivity extends BaseActivity implements WebViewCallback, String url = getCurrentWebView().getUrl(); if (url != null && !url.equals(HOME_URL)) { final long timeStamp = System.currentTimeMillis(); - SimpleDateFormat sdf = new SimpleDateFormat("d MMM yyyy", LanguageUtils.getCurrentLocale(this)); + SimpleDateFormat sdf = + new SimpleDateFormat("d MMM yyyy", LanguageUtils.getCurrentLocale(this)); HistoryListItem.HistoryItem history = new HistoryListItem.HistoryItem( 0L, ZimContentProvider.getId(), diff --git a/app/src/main/java/org/kiwix/kiwixmobile/main/OnSwipeTouchListener.java b/app/src/main/java/org/kiwix/kiwixmobile/main/OnSwipeTouchListener.java new file mode 100644 index 000000000..90ec49569 --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/main/OnSwipeTouchListener.java @@ -0,0 +1,73 @@ +package org.kiwix.kiwixmobile.main; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; + +public class OnSwipeTouchListener implements View.OnTouchListener { + private final GestureDetector gestureDetector; + + @SuppressLint("SyntheticAccessor") + public OnSwipeTouchListener(Context ctx) { + gestureDetector = new GestureDetector(ctx, new GestureListener()); + } + + @Override + @SuppressLint("ClickableViewAccessibility") + public boolean onTouch(View v, MotionEvent event) { + return gestureDetector.onTouchEvent(event); + } + + private final class GestureListener extends GestureDetector.SimpleOnGestureListener { + private static final int SWIPE_THRESHOLD = 100; + private static final int SWIPE_VELOCITY_THRESHOLD = 100; + + @Override + public boolean onDown(MotionEvent e) { + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + boolean result = false; + try { + float diffY = e2.getY() - e1.getY(); + float diffX = e2.getX() - e1.getX(); + if (Math.abs(diffX) > Math.abs(diffY)) { + if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { + if (diffX > 0) { + onSwipeRight(); + } else { + onSwipeLeft(); + } + result = true; + } + } else if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) { + if (diffY > 0) { + onSwipeBottom(); + } else { + onSwipeTop(); + } + result = true; + } + } catch (Exception exception) { + exception.printStackTrace(); + } + return result; + } + } + + public void onSwipeRight() { + } + + public void onSwipeLeft() { + } + + public void onSwipeTop() { + } + + public void onSwipeBottom() { + } +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixSettingsActivity.java b/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixSettingsActivity.java index 4f5be6392..653ef3d26 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixSettingsActivity.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixSettingsActivity.java @@ -19,9 +19,9 @@ package org.kiwix.kiwixmobile.settings; -import android.app.FragmentManager; import android.content.Intent; import android.content.SharedPreferences; +import android.content.pm.PackageManager; import android.os.Bundle; import android.os.Environment; import android.preference.EditTextPreference; @@ -35,6 +35,7 @@ import android.webkit.WebView; import android.widget.BaseAdapter; import android.widget.Toast; import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import eu.mhutti1.utils.storage.StorageDevice; import eu.mhutti1.utils.storage.StorageSelectDialog; @@ -200,7 +201,7 @@ public class KiwixSettingsActivity extends BaseActivity { String selectedLang = sharedPreferenceUtil.getPrefLanguage(Locale.getDefault().toString()); List languageCodeList = new LanguageUtils(getActivity()).getKeys(); selectedLang = languageCodeList.contains(selectedLang) ? selectedLang : "en"; - String code[] = languageCodeList.toArray(new String[languageCodeList.size()]); + String code[] = languageCodeList.toArray(new String[0]); String[] entries = new String[code.length]; for (int index = 0; index < code.length; index++) { Locale locale = new Locale(code[index]); @@ -229,11 +230,16 @@ public class KiwixSettingsActivity extends BaseActivity { } private void setAppVersionNumber() { - String version; - version = BuildConfig.VERSION_NAME + " Build: " + BuildConfig.VERSION_CODE; - EditTextPreference versionPref = (EditTextPreference) PrefsFragment.this - .findPreference(PREF_VERSION); - versionPref.setSummary(version); + EditTextPreference versionPref = (EditTextPreference) findPreference(PREF_VERSION); + versionPref.setSummary(BuildConfig.VERSION_NAME + " Build: " + getVersionCode()); + } + + private int getVersionCode() { + try { + return getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0).versionCode; + } catch (PackageManager.NameNotFoundException e) { + throw new RuntimeException(e); + } } @Override @@ -322,7 +328,6 @@ public class KiwixSettingsActivity extends BaseActivity { } public void openFolderSelect() { - FragmentManager fm = getFragmentManager(); StorageSelectDialog dialogFragment = new StorageSelectDialog(); Bundle b = new Bundle(); b.putString(StorageSelectDialog.STORAGE_DIALOG_INTERNAL, @@ -332,7 +337,7 @@ public class KiwixSettingsActivity extends BaseActivity { b.putInt(StorageSelectDialog.STORAGE_DIALOG_THEME, StyleUtils.dialogStyle()); dialogFragment.setArguments(b); dialogFragment.setOnSelectListener(this); - dialogFragment.show(fm, getResources().getString(R.string.pref_storage)); + dialogFragment.show(((AppCompatActivity) getActivity()).getSupportFragmentManager(), getResources().getString(R.string.pref_storage)); } @Override diff --git a/app/src/main/java/org/kiwix/kiwixmobile/utils/Constants.java b/app/src/main/java/org/kiwix/kiwixmobile/utils/Constants.java index 64dd83e99..5b070dec0 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/utils/Constants.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/utils/Constants.java @@ -66,8 +66,6 @@ public final class Constants { public static final String PREF_WIFI_ONLY = "pref_wifi_only"; - public static final String PREF_BOTTOM_TOOLBAR = "pref_bottomtoolbar"; - public static final String PREF_KIWIX_MOBILE = "kiwix-mobile"; public static final String PREF_BACK_TO_TOP = "pref_backtotop"; diff --git a/app/src/main/java/org/kiwix/kiwixmobile/utils/SharedPreferenceUtil.java b/app/src/main/java/org/kiwix/kiwixmobile/utils/SharedPreferenceUtil.java index fd9858bd9..e3d8b6925 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/utils/SharedPreferenceUtil.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/utils/SharedPreferenceUtil.java @@ -4,14 +4,15 @@ import android.content.Context; import android.content.SharedPreferences; import android.os.Environment; import android.preference.PreferenceManager; +import io.reactivex.Flowable; import io.reactivex.processors.BehaviorProcessor; +import io.reactivex.processors.PublishProcessor; import java.util.Calendar; import javax.inject.Inject; import javax.inject.Singleton; import static org.kiwix.kiwixmobile.utils.Constants.PREF_AUTONIGHTMODE; import static org.kiwix.kiwixmobile.utils.Constants.PREF_BACK_TO_TOP; -import static org.kiwix.kiwixmobile.utils.Constants.PREF_BOTTOM_TOOLBAR; import static org.kiwix.kiwixmobile.utils.Constants.PREF_EXTERNAL_LINK_POPUP; import static org.kiwix.kiwixmobile.utils.Constants.PREF_FULLSCREEN; import static org.kiwix.kiwixmobile.utils.Constants.PREF_HIDE_TOOLBAR; @@ -35,12 +36,11 @@ public class SharedPreferenceUtil { private static final String PREF_SHOW_BOOKMARKS_CURRENT_BOOK = "show_bookmarks_current_book"; private static final String PREF_SHOW_HISTORY_CURRENT_BOOK = "show_history_current_book"; private SharedPreferences sharedPreferences; - public final BehaviorProcessor prefStorages; + private final PublishProcessor prefStorages = PublishProcessor.create(); @Inject public SharedPreferenceUtil(Context context) { sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); - prefStorages = BehaviorProcessor.createDefault(getPrefStorage()); } public boolean getPrefWifiOnly() { @@ -59,9 +59,6 @@ public class SharedPreferenceUtil { return sharedPreferences.getBoolean(PREF_FULLSCREEN, false); } - public boolean getPrefBottomToolbar() { - return sharedPreferences.getBoolean(PREF_BOTTOM_TOOLBAR, false); - } public boolean getPrefBackToTop() { return sharedPreferences.getBoolean(PREF_BACK_TO_TOP, false); @@ -130,6 +127,10 @@ public class SharedPreferenceUtil { prefStorages.onNext(storage); } + public Flowable getPrefStorages(){ + return prefStorages.startWith(getPrefStorage()); + } + public void putPrefFullScreen(boolean fullScreen) { sharedPreferences.edit().putBoolean(PREF_FULLSCREEN, fullScreen).apply(); } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/utils/files/FileSearch.kt b/app/src/main/java/org/kiwix/kiwixmobile/utils/files/FileSearch.kt index 25f7f56fe..9c47b6ab1 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/utils/files/FileSearch.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/utils/files/FileSearch.kt @@ -26,6 +26,7 @@ import android.provider.MediaStore.MediaColumns import eu.mhutti1.utils.storage.StorageDeviceUtils import io.reactivex.Flowable import io.reactivex.functions.BiFunction +import io.reactivex.schedulers.Schedulers import org.kiwix.kiwixmobile.extensions.forEachRow import org.kiwix.kiwixmobile.extensions.get import java.io.File @@ -37,8 +38,8 @@ class FileSearch @Inject constructor(private val context: Context) { fun scan(defaultPath: String) = Flowable.combineLatest( - Flowable.fromCallable { scanFileSystem(defaultPath) }, - Flowable.fromCallable(this::scanMediaStore), + Flowable.fromCallable { scanFileSystem(defaultPath) }.subscribeOn(Schedulers.io()), + Flowable.fromCallable(this::scanMediaStore).subscribeOn(Schedulers.io()), BiFunction, List, List> { filesSystemFiles, mediaStoreFiles -> filesSystemFiles + mediaStoreFiles } @@ -74,13 +75,19 @@ class FileSearch @Inject constructor(private val context: Context) { *StorageDeviceUtils.getStorageDevices(context, false).map { it.name }.toTypedArray() ) - private fun scanDirectory(directory: String) = filesMatchingExtensions(directory) ?: emptyList() - - private fun filesMatchingExtensions(directory: String) = File(directory) - .listFiles { _, name -> name.endsWithAny(*zimFileExtensions) } - ?.toList() + private fun scanDirectory(directory: String): List = File(directory).listFiles() + ?.fold( + mutableListOf(), { acc, file -> + acc.apply { + if (file.isDirectory) { + addAll(scanDirectory(file.path)) + } else if (file.extension.isAny(*zimFileExtensions)) { + add(file) + } + } + }) ?: emptyList() } -internal fun String.endsWithAny(vararg suffixes: String) = - suffixes.fold(false, { acc, s -> acc or endsWith(s) }) +internal fun String.isAny(vararg suffixes: String) = + suffixes.firstOrNull { endsWith(it) } != null diff --git a/app/src/main/java/org/kiwix/kiwixmobile/views/LanguageAdapter.kt b/app/src/main/java/org/kiwix/kiwixmobile/views/LanguageAdapter.kt deleted file mode 100644 index 668adb82b..000000000 --- a/app/src/main/java/org/kiwix/kiwixmobile/views/LanguageAdapter.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.kiwix.kiwixmobile.views - - -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import org.kiwix.kiwixmobile.R -import org.kiwix.kiwixmobile.extensions.inflate -import org.kiwix.kiwixmobile.zim_manager.Language - -class LanguageAdapter(val listItems: MutableList) : RecyclerView.Adapter() { - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ) = LanguageViewHolder( - parent.inflate(R.layout.item_language, false), - this::toggleItemAt - ) - - override fun getItemCount() = listItems.size - - override fun onBindViewHolder( - holder: LanguageViewHolder, - position: Int - ) { - holder.bind(listItems[position], position) - } - - private fun toggleItemAt(position: Int) { - listItems[position] = listItems[position].also { it.active = !it.active } - notifyItemChanged(position) - } -} - diff --git a/app/src/main/java/org/kiwix/kiwixmobile/views/LanguageSelectDialog.kt b/app/src/main/java/org/kiwix/kiwixmobile/views/LanguageSelectDialog.kt deleted file mode 100644 index c74d01928..000000000 --- a/app/src/main/java/org/kiwix/kiwixmobile/views/LanguageSelectDialog.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Kiwix Android - * Copyright (C) 2018 Kiwix - * - * 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 . - */ -package org.kiwix.kiwixmobile.views - -import android.content.Context -import android.view.View -import androidx.appcompat.app.AlertDialog -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.language_selection.language_check_view -import org.kiwix.kiwixmobile.R -import org.kiwix.kiwixmobile.zim_manager.Language - -/** - * Created by judebrauer on 12/6/17 - */ - -class LanguageSelectDialog constructor( - context: Context -) : AlertDialog(context) { - - class Builder : AlertDialog.Builder, LayoutContainer { - lateinit var dialogView: View - override val containerView: View? by lazy { dialogView } - lateinit var onOkClicked: (List) -> Unit - var languages: List = listOf() - - constructor(context: Context) : super(context) - - constructor( - context: Context, - themeResId: Int - ) : super(context, themeResId) - - override fun create(): AlertDialog { - dialogView = View.inflate(context, R.layout.language_selection, null) - val languageArrayAdapter = LanguageAdapter(languages.toMutableList()) - language_check_view.run { - adapter = languageArrayAdapter - layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - setHasFixedSize(true) - } - setView(dialogView) - setPositiveButton(android.R.string.ok) { _, _ -> - onOkClicked.invoke(languageArrayAdapter.listItems) - } - return super.create() - } - } -} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/views/LanguageViewHolder.kt b/app/src/main/java/org/kiwix/kiwixmobile/views/LanguageViewHolder.kt deleted file mode 100644 index fdee12c76..000000000 --- a/app/src/main/java/org/kiwix/kiwixmobile/views/LanguageViewHolder.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.kiwix.kiwixmobile.views - -import android.graphics.Typeface -import android.view.View -import androidx.recyclerview.widget.RecyclerView.ViewHolder -import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.item_language.item_language_books_count -import kotlinx.android.synthetic.main.item_language.item_language_checkbox -import kotlinx.android.synthetic.main.item_language.item_language_localized_name -import kotlinx.android.synthetic.main.item_language.item_language_name -import org.kiwix.kiwixmobile.R -import org.kiwix.kiwixmobile.utils.LanguageUtils -import org.kiwix.kiwixmobile.zim_manager.Language - -class LanguageViewHolder( - override val containerView: View, - private val onCheckboxChecked: (Int) -> Unit -) : ViewHolder(containerView), - LayoutContainer { - fun bind( - language: Language, - position: Int - ) { - val context = containerView.context - item_language_name.text = language.language - item_language_localized_name.text = context.getString( - R.string.language_localized, - language.languageLocalized - ) - item_language_localized_name.typeface = Typeface.createFromAsset( - context.assets, - LanguageUtils.getTypeface(language.languageCode) - ) - item_language_books_count.text = - context.getString(R.string.language_count, language.occurencesOfLanguage) - item_language_checkbox.setOnCheckedChangeListener(null) - item_language_checkbox.isChecked = language.active - item_language_checkbox.setOnCheckedChangeListener { _, _ -> - onCheckboxChecked.invoke(position) - } - containerView.setOnClickListener { - item_language_checkbox.toggle() - } - } -} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/Language.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/Language.kt index 172f6491d..7465a74a2 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/Language.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/Language.kt @@ -6,6 +6,7 @@ import java.util.Locale @Parcelize data class Language constructor( + val id: Long = 0, var active: Boolean, var occurencesOfLanguage: Int, var language: String, @@ -13,11 +14,14 @@ data class Language constructor( var languageCode: String, var languageCodeISO2: String ) : Parcelable { + constructor( locale: Locale, active: Boolean, - occurrencesOfLanguage: Int + occurrencesOfLanguage: Int, + id: Long = 0 ) : this( + id, active, occurrencesOfLanguage, locale.displayLanguage, @@ -35,4 +39,7 @@ data class Language constructor( override fun equals(other: Any?): Boolean { return (other as Language).language == language && other.active == active } + + fun matches(filter: String) = + language.contains(filter, true) or languageLocalized.contains(filter, true) } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/ZimManageActivity.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/ZimManageActivity.kt index cf9f15bfa..c572919ac 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/ZimManageActivity.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/ZimManageActivity.kt @@ -26,33 +26,26 @@ import android.util.Log import android.view.Menu import android.view.MenuItem import androidx.appcompat.widget.SearchView -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProviders -import io.reactivex.Flowable -import io.reactivex.schedulers.Schedulers import kotlinx.android.synthetic.main.zim_manager.manageViewPager import kotlinx.android.synthetic.main.zim_manager.tabs import kotlinx.android.synthetic.main.zim_manager.toolbar import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.base.BaseActivity import org.kiwix.kiwixmobile.database.newdb.dao.NewLanguagesDao -import org.kiwix.kiwixmobile.extensions.toast +import org.kiwix.kiwixmobile.extensions.start +import org.kiwix.kiwixmobile.extensions.viewModel +import org.kiwix.kiwixmobile.language.LanguageActivity import org.kiwix.kiwixmobile.main.MainActivity import org.kiwix.kiwixmobile.utils.Constants.TAG_KIWIX import org.kiwix.kiwixmobile.utils.LanguageUtils -import org.kiwix.kiwixmobile.utils.StyleUtils.dialogStyle -import org.kiwix.kiwixmobile.views.LanguageSelectDialog import java.io.File import javax.inject.Inject import org.kiwix.kiwixmobile.zim_manager.local_file_transfer.LocalFileTransferActivity; class ZimManageActivity : BaseActivity() { - private val zimManageViewModel: ZimManageViewModel by lazy { - ViewModelProviders.of(this, viewModelFactory) - .get(ZimManageViewModel::class.java) - } + private val zimManageViewModel by lazy { viewModel(viewModelFactory) } private val mSectionsPagerAdapter: SectionsPagerAdapter by lazy { SectionsPagerAdapter(this, supportFragmentManager) } @@ -79,9 +72,6 @@ class ZimManageActivity : BaseActivity() { tabs.setupWithViewPager(this) addOnPageChangeListener(SimplePageChangeListener(this@ZimManageActivity::updateMenu)) } - zimManageViewModel.languageItems.observe(this, Observer { - onLanguageItemsForDialogUpdated(it!!) - }) setViewPagerPositionFromIntent(intent) } @@ -96,24 +86,6 @@ class ZimManageActivity : BaseActivity() { } } - private fun onLanguageItemsForDialogUpdated(languages: List) { - if (languages.isEmpty()) { - toast(R.string.wait_for_load) - } else { - LanguageSelectDialog.Builder(this, dialogStyle()) - .apply { - onOkClicked = { - Flowable.fromCallable { - languagesDao.insert(it) - } - .subscribeOn(Schedulers.io()) - .subscribe() - } - this.languages = languages - } - .show() - } - } private fun updateMenu(position: Int) { searchItem?.isVisible = position == 1 @@ -156,9 +128,8 @@ class ZimManageActivity : BaseActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.select_language -> { - zimManageViewModel.requestLanguagesDialog.onNext(Unit) - } + R.id.select_language -> start() + R.id.get_zim_nearby_device -> { startActivity(Intent(this, LocalFileTransferActivity::class.java)); } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/ZimManageViewModel.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/ZimManageViewModel.kt index 88a77ff75..1f92adbda 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/ZimManageViewModel.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/ZimManageViewModel.kt @@ -110,13 +110,11 @@ class ZimManageViewModel @Inject constructor( val deviceListIsRefreshing = MutableLiveData() val libraryListIsRefreshing = MutableLiveData() val networkStates = MutableLiveData() - val languageItems = MutableLiveData>() val requestFileSystemCheck = PublishProcessor.create() val fileSelectActions = PublishProcessor.create() val requestDownloadLibrary = BehaviorProcessor.createDefault(Unit) val requestFiltering = BehaviorProcessor.createDefault("") - val requestLanguagesDialog = PublishProcessor.create() private val compositeDisposable = CompositeDisposable() @@ -151,7 +149,6 @@ class ZimManageViewModel @Inject constructor( updateLibraryItems(booksFromDao, downloads, networkLibrary, languages), updateLanguagesInDao(networkLibrary, languages), updateNetworkStates(), - updateLanguageItemsForDialog(languages), requestsAndConnectivtyChangesToLibraryRequests(networkLibrary), fileSelectActions() ) @@ -286,16 +283,6 @@ class ZimManageViewModel @Inject constructor( ) } - private fun updateLanguageItemsForDialog(languages: Flowable>) = - requestLanguagesDialog - .withLatestFrom( - languages, - BiFunction, List> { _, langs -> langs }) - .subscribe( - languageItems::postValue, - Throwable::printStackTrace - ) - private fun updateNetworkStates() = connectivityBroadcastReceiver.networkStates.subscribe( networkStates::postValue, Throwable::printStackTrace diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/StorageObserver.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/StorageObserver.kt index 8552bdd61..bf6d6852b 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/StorageObserver.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/StorageObserver.kt @@ -14,11 +14,11 @@ import javax.inject.Inject class StorageObserver @Inject constructor( private val sharedPreferenceUtil: SharedPreferenceUtil, - downloadDao: NewDownloadDao, + private val downloadDao: NewDownloadDao, private val fileSearch: FileSearch ) { - val booksOnFileSystem = scanFiles() + val booksOnFileSystem get() = scanFiles() .withLatestFrom( downloadDao.downloads(), BiFunction(this::toFilesThatAreNotDownloading) diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/ZimFileSelectFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/ZimFileSelectFragment.kt index 854f3a55c..78642abf7 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/ZimFileSelectFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/ZimFileSelectFragment.kt @@ -29,7 +29,6 @@ import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.reactivex.disposables.CompositeDisposable @@ -40,6 +39,7 @@ import org.kiwix.kiwixmobile.R import org.kiwix.kiwixmobile.base.BaseFragment import org.kiwix.kiwixmobile.di.components.ActivityComponent import org.kiwix.kiwixmobile.extensions.toast +import org.kiwix.kiwixmobile.extensions.viewModel import org.kiwix.kiwixmobile.utils.Constants.REQUEST_STORAGE_PERMISSION import org.kiwix.kiwixmobile.utils.LanguageUtils import org.kiwix.kiwixmobile.utils.SharedPreferenceUtil @@ -61,11 +61,9 @@ class ZimFileSelectFragment : BaseFragment() { private var actionMode: ActionMode? = null val disposable = CompositeDisposable() - private val zimManageViewModel: ZimManageViewModel by lazy { - ViewModelProviders.of(activity!!, viewModelFactory) - .get(ZimManageViewModel::class.java) + private val zimManageViewModel by lazy { + activity!!.viewModel(viewModelFactory) } - private val bookDelegate: BookDelegate by lazy { BookDelegate(sharedPreferenceUtil, { offerAction(RequestOpen(it)) }, diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/adapter/BookOnDiskDelegate.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/adapter/BookOnDiskDelegate.kt index f1a1fe3a6..9987af2fe 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/adapter/BookOnDiskDelegate.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/fileselect_view/adapter/BookOnDiskDelegate.kt @@ -47,7 +47,7 @@ sealed class BookOnDiskDelegate(containerView: View) when (selectionMode) { MULTI -> { itemBookCheckbox.visibility = View.VISIBLE - containerView.setOnClickListener { multiSelectAction?.invoke(item) } - containerView.setOnLongClickListener(null) + item_book_clickable_area.setOnClickListener { multiSelectAction?.invoke(item) } + item_book_clickable_area.setOnLongClickListener(null) } NORMAL -> { itemBookCheckbox.visibility = View.GONE - containerView.setOnClickListener { clickAction.invoke(item) } - containerView.setOnLongClickListener { + item_book_clickable_area.setOnClickListener { clickAction.invoke(item) } + item_book_clickable_area.setOnLongClickListener { longClickAction?.invoke(item) return@setOnLongClickListener true } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/LibraryFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/LibraryFragment.kt index 7b9e2edb4..3ec2e70ca 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/LibraryFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/LibraryFragment.kt @@ -17,6 +17,7 @@ */ package org.kiwix.kiwixmobile.zim_manager.library_view +import android.annotation.SuppressLint import android.net.ConnectivityManager import android.os.Bundle import android.view.LayoutInflater @@ -26,11 +27,10 @@ import android.view.View.VISIBLE import android.view.ViewGroup import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelProviders import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import eu.mhutti1.utils.storage.StorageDevice -import eu.mhutti1.utils.storage.support.StorageSelectDialog +import eu.mhutti1.utils.storage.StorageSelectDialog import kotlinx.android.synthetic.main.activity_library.libraryErrorText import kotlinx.android.synthetic.main.activity_library.libraryList import kotlinx.android.synthetic.main.activity_library.librarySwipeRefresh @@ -41,6 +41,7 @@ import org.kiwix.kiwixmobile.di.components.ActivityComponent import org.kiwix.kiwixmobile.downloader.Downloader import org.kiwix.kiwixmobile.extensions.snack import org.kiwix.kiwixmobile.extensions.toast +import org.kiwix.kiwixmobile.extensions.viewModel import org.kiwix.kiwixmobile.library.entity.LibraryNetworkEntity.Book import org.kiwix.kiwixmobile.main.MainActivity import org.kiwix.kiwixmobile.utils.BookUtils @@ -71,10 +72,10 @@ class LibraryFragment : BaseFragment() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory @Inject lateinit var bookUtils: BookUtils - private val zimManageViewModel: ZimManageViewModel by lazy { - ViewModelProviders.of(activity!!, viewModelFactory) - .get(ZimManageViewModel::class.java) + private val zimManageViewModel by lazy { + activity!!.viewModel(viewModelFactory) } + private val libraryAdapter: LibraryAdapter by lazy { LibraryAdapter( BookDelegate(bookUtils, this::onBookItemClick), DividerDelegate @@ -209,8 +210,10 @@ class LibraryFragment : BaseFragment() { private fun notEnoughSpaceAvailable(item: BookItem) = spaceAvailable < item.book.size.toLong() * 1024f + @SuppressLint("ImplicitSamInstance") private fun showStorageSelectDialog() { - StorageSelectDialog().apply { + StorageSelectDialog() + .apply { arguments = Bundle().apply { putString( StorageSelectDialog.STORAGE_DIALOG_INTERNAL, diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/adapter/base/AbsDelegateAdapter.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/adapter/base/AbsDelegateAdapter.kt index 00105fcad..4adaf5a64 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/adapter/base/AbsDelegateAdapter.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/adapter/base/AbsDelegateAdapter.kt @@ -25,7 +25,10 @@ interface AbsDelegateAdapter> : AdapterDelegate { - abstract val itemClass: Class + + val itemClass: Class + + @Suppress("UNCHECKED_CAST") override fun bind( viewHolder: RecyclerView.ViewHolder, itemToBind: SUPERTYPE diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/adapter/base/AdapterDelegateManager.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/adapter/base/AdapterDelegateManager.kt index de671d953..6fa79c5c1 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/adapter/base/AdapterDelegateManager.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/adapter/base/AdapterDelegateManager.kt @@ -27,7 +27,7 @@ class AdapterDelegateManager() { private fun getDelegateIndexFor(item: T): Int { for (index in 0..delegates.size()) { val valueAt = delegates.valueAt(index) - if (valueAt.isFor(item)) { + if (valueAt?.isFor(item) == true) { return index; } } diff --git a/app/src/main/res/layout/activity_bookmarks_history_language.xml b/app/src/main/res/layout/activity_bookmarks.xml similarity index 94% rename from app/src/main/res/layout/activity_bookmarks_history_language.xml rename to app/src/main/res/layout/activity_bookmarks.xml index b8afd1c47..71ef17dd9 100644 --- a/app/src/main/res/layout/activity_bookmarks_history_language.xml +++ b/app/src/main/res/layout/activity_bookmarks.xml @@ -3,7 +3,6 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?attr/listBackground" android:fitsSystemWindows="true" > @@ -26,7 +25,7 @@ + diff --git a/app/src/main/res/layout/activity_history.xml b/app/src/main/res/layout/activity_history.xml new file mode 100644 index 000000000..71ef17dd9 --- /dev/null +++ b/app/src/main/res/layout/activity_history.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_language.xml b/app/src/main/res/layout/activity_language.xml new file mode 100644 index 000000000..ab3646d3c --- /dev/null +++ b/app/src/main/res/layout/activity_language.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/device_item.xml b/app/src/main/res/layout/device_item.xml new file mode 100644 index 000000000..89c51c432 --- /dev/null +++ b/app/src/main/res/layout/device_item.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_book.xml b/app/src/main/res/layout/item_book.xml index eb8c48d3c..54e11e185 100644 --- a/app/src/main/res/layout/item_book.xml +++ b/app/src/main/res/layout/item_book.xml @@ -4,7 +4,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?android:attr/selectableItemBackground" android:paddingEnd="0dp" android:paddingLeft="0dp" android:paddingRight="0dp" @@ -135,5 +134,14 @@ app:layout_constraintStart_toStartOf="@id/item_book_title" app:layout_constraintTop_toBottomOf="@id/item_book_label_picture" /> - + diff --git a/app/src/main/res/layout/item_language.xml b/app/src/main/res/layout/item_language.xml index 3e367bcf5..343ac22a1 100644 --- a/app/src/main/res/layout/item_language.xml +++ b/app/src/main/res/layout/item_language.xml @@ -57,4 +57,9 @@ app:layout_constraintTop_toTopOf="parent" tools:text="9 books" /> + diff --git a/app/src/main/res/layout/language_selection.xml b/app/src/main/res/layout/language_selection.xml deleted file mode 100644 index 26dd7b23b..000000000 --- a/app/src/main/res/layout/language_selection.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/search.xml b/app/src/main/res/layout/search.xml index a2088fd43..b4346faa0 100644 --- a/app/src/main/res/layout/search.xml +++ b/app/src/main/res/layout/search.xml @@ -15,7 +15,6 @@ diff --git a/app/src/main/res/layout/storage_select_dialog.xml b/app/src/main/res/layout/storage_select_dialog.xml new file mode 100644 index 000000000..7e75b03f5 --- /dev/null +++ b/app/src/main/res/layout/storage_select_dialog.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + +