64 Custom apps should validate their expansion files and re-download if corrupt/missing - add Custom Download flow

This commit is contained in:
Sean Mac Gillicuddy 2019-11-05 11:30:13 +00:00
parent fed540a390
commit c05742d02a
43 changed files with 844 additions and 158 deletions

View File

@ -19,40 +19,17 @@
package org.kiwix.kiwixmobile;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Provider;
import org.kiwix.kiwixmobile.core.ViewModelFactory;
import org.kiwix.kiwixmobile.di.KiwixScope;
@KiwixScope
public class KiwixViewModelFactory implements ViewModelProvider.Factory {
private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;
public class KiwixViewModelFactory extends ViewModelFactory {
@Inject
public KiwixViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
this.creators = creators;
}
@SuppressWarnings("unchecked")
@Override
public <T extends ViewModel> T create(Class<T> modelClass) {
Provider<? extends ViewModel> creator = creators.get(modelClass);
if (creator == null) {
for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) {
if (modelClass.isAssignableFrom(entry.getKey())) {
creator = entry.getValue();
break;
}
}
}
if (creator == null) {
throw new IllegalArgumentException("unknown model class " + modelClass);
}
try {
return (T) creator.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
super(creators);
}
}

View File

@ -22,6 +22,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.processors.PublishProcessor
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.language.adapter.LanguageListItem.LanguageItem
import org.kiwix.kiwixmobile.language.viewmodel.Action.Filter
@ -31,7 +32,6 @@ 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(

View File

@ -22,7 +22,7 @@ import io.reactivex.Flowable
import io.reactivex.schedulers.Schedulers
import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao
import org.kiwix.kiwixmobile.core.zim_manager.Language
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.SideEffect
import org.kiwix.kiwixmobile.core.base.SideEffect
data class SaveLanguagesAndFinish(
val languages: List<Language>,

View File

@ -66,7 +66,7 @@ import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.DeleteFiles
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.None
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.OpenFile
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.ShareFiles
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.SideEffect
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.StartMultiSelection
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem.BookItem

View File

@ -17,7 +17,6 @@
*/
package org.kiwix.kiwixmobile.zim_manager.download_view
import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer
@ -29,12 +28,6 @@ import kotlinx.android.synthetic.main.download_item.favicon
import kotlinx.android.synthetic.main.download_item.stop
import kotlinx.android.synthetic.main.download_item.title
import org.kiwix.kiwixmobile.core.downloader.model.DownloadItem
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState.Failed
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState.Paused
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState.Pending
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState.Running
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState.Successful
import org.kiwix.kiwixmobile.core.extensions.setBitmap
class DownloadViewHolder(override val containerView: View) : RecyclerView.ViewHolder(containerView),
@ -50,21 +43,7 @@ class DownloadViewHolder(override val containerView: View) : RecyclerView.ViewHo
stop.setOnClickListener {
itemClickListener.invoke(downloadItem)
}
downloadState.text = toReadableState(downloadItem.downloadState, containerView.context)
eta.text = downloadItem.eta.takeIf { it.seconds > 0L }?.toHumanReadableTime() ?: ""
}
private fun toReadableState(
downloadState: DownloadState,
context: Context
) = when (downloadState) {
is Failed -> context.getString(
downloadState.stringId,
downloadState.reason.name
)
Pending,
Running,
Paused,
Successful -> context.getString(downloadState.stringId)
downloadState.text = downloadItem.downloadState.toReadableState(containerView.context)
eta.text = downloadItem.readableEta
}
}

View File

@ -20,6 +20,7 @@ package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects
import android.app.Activity
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer

View File

@ -19,6 +19,7 @@
package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects
import android.app.Activity
import org.kiwix.kiwixmobile.core.base.SideEffect
object None : SideEffect<Unit> {
override fun invokeWith(activity: Activity) {

View File

@ -20,12 +20,14 @@ package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects
import android.app.Activity
import androidx.core.net.toUri
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.extensions.start
import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
import org.kiwix.kiwixmobile.main.KiwixMainActivity
class OpenFile(private val bookOnDisk: BookOnDisk) : SideEffect<Unit> {
class OpenFile(private val bookOnDisk: BookOnDisk) :
SideEffect<Unit> {
override fun invokeWith(activity: Activity) {
val file = bookOnDisk.file

View File

@ -24,9 +24,11 @@ import android.net.Uri
import android.os.Build
import androidx.core.content.FileProvider
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk
class ShareFiles(private val selectedBooks: List<BookOnDisk>) : SideEffect<Unit> {
class ShareFiles(private val selectedBooks: List<BookOnDisk>) :
SideEffect<Unit> {
override fun invokeWith(activity: Activity) {
val selectedFileShareIntent = Intent()
selectedFileShareIntent.action = Intent.ACTION_SEND_MULTIPLE

View File

@ -22,6 +22,7 @@ import android.app.Activity
import android.view.ActionMode
import io.reactivex.processors.PublishProcessor
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.extensions.startActionMode
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions

View File

@ -143,6 +143,7 @@ class AllProjectConfigurer {
implementation(Libs.kotlin_stdlib_jdk7)
implementation(Libs.appcompat)
implementation(Libs.material)
implementation(Libs.constraintlayout)
implementation(Libs.androidx_multidex_multidex)
implementation(Libs.okhttp)
implementation(Libs.logging_interceptor)

View File

@ -46,8 +46,6 @@ dependencies {
// Android Support
implementation(Libs.cardview)
implementation(Libs.constraintlayout)
// Tab indicator
implementation(Libs.ink_page_indicator)

View File

@ -21,10 +21,10 @@
android:hardwareAccelerated="true"
android:icon="@mipmap/kiwix_icon"
android:label="@string/app_name"
android:usesCleartextTraffic="true"
android:roundIcon="@mipmap/kiwix_icon_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
tools:targetApi="m">
<activity
@ -35,6 +35,7 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.MONKEY" />
</intent-filter>

View File

@ -0,0 +1,55 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import java.util.Map;
import javax.inject.Provider;
public class ViewModelFactory implements ViewModelProvider.Factory {
private final Map<Class<? extends ViewModel>, Provider<ViewModel>> creators;
public ViewModelFactory(
Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
this.creators = creators;
}
@SuppressWarnings("unchecked")
@Override
public <T extends ViewModel> T create(Class<T> modelClass) {
Provider<? extends ViewModel> creator = creators.get(modelClass);
if (creator == null) {
for (Map.Entry<Class<? extends ViewModel>, Provider<ViewModel>> entry : creators.entrySet()) {
if (modelClass.isAssignableFrom(entry.getKey())) {
creator = entry.getValue();
break;
}
}
}
if (creator == null) {
throw new IllegalArgumentException("unknown model class " + modelClass);
}
try {
return (T) creator.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects
package org.kiwix.kiwixmobile.core.base
import android.app.Activity

View File

@ -17,6 +17,7 @@
*/
package org.kiwix.kiwixmobile.core.downloader.model
import android.content.Context
import com.tonyodev.fetch2.Error
import com.tonyodev.fetch2.Status
import com.tonyodev.fetch2.Status.ADDED
@ -43,6 +44,8 @@ data class DownloadItem(
val downloadState: DownloadState
) {
val readableEta: CharSequence = eta.takeIf { it.seconds > 0L }?.toHumanReadableTime() ?: ""
constructor(downloadModel: DownloadModel) : this(
downloadModel.downloadId,
Base64String(downloadModel.book.favicon),
@ -84,4 +87,12 @@ sealed class DownloadState(val stringId: Int) {
data class Failed(val reason: Error) : DownloadState(R.string.failed_state)
override fun toString(): String = javaClass.simpleName
fun toReadableState(context: Context): CharSequence = when (this) {
is Failed -> context.getString(stringId, reason.name)
Pending,
Running,
Paused,
Successful -> context.getString(stringId)
}
}

View File

@ -0,0 +1,27 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.extensions
import android.widget.ViewAnimator
fun ViewAnimator.setDistinctDisplayedChild(index: Int) {
if (displayedChild != index) {
displayedChild = index
}
}

View File

@ -1091,8 +1091,7 @@ public abstract class CoreMainActivity extends BaseActivity implements WebViewCa
@NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_STORAGE_PERMISSION: {
if (grantResults.length > 0
&& grantResults[0] == PERMISSION_GRANTED) {
if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) {
finish();
Intent newZimFile = Intents.internal(CoreMainActivity.class);
newZimFile.setData(Uri.fromFile(file));
@ -1222,7 +1221,6 @@ public abstract class CoreMainActivity extends BaseActivity implements WebViewCa
}, 300);
}
@OnClick(R2.id.bottom_toolbar_bookmark)
public void toggleBookmark() {
//Check maybe need refresh
@ -1605,7 +1603,7 @@ public abstract class CoreMainActivity extends BaseActivity implements WebViewCa
return true;
}
protected boolean urlIsInvalid(){
protected boolean urlIsInvalid() {
return getCurrentWebView().getUrl() == null;
}
@ -1827,14 +1825,10 @@ public abstract class CoreMainActivity extends BaseActivity implements WebViewCa
}
private void searchFiles() {
if (Build.VERSION.SDK_INT >= VERSION_CODES.M && ContextCompat.checkSelfPermission(this,
Manifest.permission.READ_EXTERNAL_STORAGE)
!= PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[] { Manifest.permission.READ_EXTERNAL_STORAGE },
REQUEST_READ_STORAGE_PERMISSION);
} else {
if (hasPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) {
scanStorageForZims();
} else {
requestExternalStoragePermission();
}
}

View File

@ -53,6 +53,7 @@
<item name="android:textColorSecondary">@color/textDarkSecondary</item>
<item name="android:textColorTertiary">@color/textDarkTertiary</item>
<item name="colorControlNormal">@color/accent</item>
<item name="buttonStyle">@style/KiwixButtonStyle</item>
</style>
<style name="AppTheme.Night" parent="AppTheme.Base">
@ -162,4 +163,8 @@
<item name="layout_constraintBottom_toBottomOf">parent</item>
</style>
<style name="KiwixButtonStyle" parent="Widget.MaterialComponents.Button">
<item name="android:textAppearance">@style/TextAppearance.AppCompat.Medium</item>
</style>
</resources>

View File

@ -109,6 +109,7 @@ android {
buildConfigField "int", "CONTENT_VERSION_CODE", "$parsedJson.version_code"
}
buildConfigField "String", "ENFORCED_LANG", "\"$parsedJson.enforced_lang\""
buildConfigField "String", "ZIM_URL", "\"$parsedJson.zim_url\""
resValue "string", "app_name", "$parsedJson.app_name"
resValue "string", "app_search_string", "Search " + "$parsedJson.app_name"
}

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission
android:name="android.permission.INTERNET"
tools:node="remove" />
</manifest>

View File

@ -3,7 +3,8 @@
"package": "org.kiwix.kiwixcustomexample",
"version_name": "2017-07",
"version_code": "1",
"zim_file": "wikipedia_fr_test_2017-07.zim",
"zim_file": "kiwix.granbluefantasy_en_all_all.zim",
"zim_url": "http://download.kiwix.org/zim/other/granbluefantasy_en_all_nopic_2018-10.zim.meta4",
"embed_zim": false,
"ic_launcher": "icon.png",
"enforced_lang": "en"

View File

@ -39,5 +39,6 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name=".download.CustomDownloadActivity" />
</application>
</manifest>

View File

@ -0,0 +1,35 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.custom;
import androidx.lifecycle.ViewModel;
import java.util.Map;
import javax.inject.Inject;
import javax.inject.Provider;
import org.kiwix.kiwixmobile.core.ViewModelFactory;
import org.kiwix.kiwixmobile.custom.di.CustomScope;
@CustomScope
public class CustomViewModelFactory extends ViewModelFactory {
@Inject
public CustomViewModelFactory(Map<Class<? extends ViewModel>, Provider<ViewModel>> creators) {
super(creators);
}
}

View File

@ -22,6 +22,7 @@ import android.app.Activity
import dagger.BindsInstance
import dagger.Subcomponent
import org.kiwix.kiwixmobile.core.di.ActivityScope
import org.kiwix.kiwixmobile.custom.download.CustomDownloadActivity
import org.kiwix.kiwixmobile.custom.main.CustomMainActivity
import org.kiwix.kiwixmobile.custom.settings.CustomSettingsActivity
@ -30,6 +31,7 @@ import org.kiwix.kiwixmobile.custom.settings.CustomSettingsActivity
interface CustomActivityComponent {
fun inject(customMainActivity: CustomMainActivity)
fun inject(customSettingsActivity: CustomSettingsActivity)
fun inject(customDownloadActivity: CustomDownloadActivity)
@Subcomponent.Builder
interface Builder {

View File

@ -21,7 +21,7 @@ package org.kiwix.kiwixmobile.custom.di
import dagger.Component
import org.kiwix.kiwixmobile.core.di.components.CoreComponent
@Component(dependencies = [CoreComponent::class])
@Component(dependencies = [CoreComponent::class], modules = [CustomViewModelModule::class])
@CustomScope
interface CustomComponent {
fun activityComponentBuilder(): CustomActivityComponent.Builder

View File

@ -0,0 +1,40 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.custom.di
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import dagger.Binds
import dagger.Module
import dagger.multibindings.IntoMap
import org.kiwix.kiwixmobile.custom.CustomViewModelFactory
import org.kiwix.kiwixmobile.custom.download.CustomDownloadViewModel
import org.kiwix.kiwixmobile.di.ViewModelKey
@Module
abstract class CustomViewModelModule {
@Binds
@IntoMap
@ViewModelKey(CustomDownloadViewModel::class)
abstract fun bindCustomDownloadViewModel(zimManageViewModel: CustomDownloadViewModel): ViewModel
@Binds
abstract fun bindViewModelFactory(factory: CustomViewModelFactory):
ViewModelProvider.Factory
}

View File

@ -0,0 +1,27 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.custom.download
import org.kiwix.kiwixmobile.core.downloader.model.DownloadItem
sealed class Action {
data class DatabaseEmission(val downloads: List<DownloadItem>) : Action()
object ClickedDownload : Action()
object ClickedRetry : Action()
}

View File

@ -0,0 +1,99 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.custom.download
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import io.reactivex.disposables.CompositeDisposable
import kotlinx.android.synthetic.main.activity_custom_download.cd_view_animator
import kotlinx.android.synthetic.main.layout_custom_download_error.cd_error_text
import kotlinx.android.synthetic.main.layout_custom_download_error.cd_retry_button
import kotlinx.android.synthetic.main.layout_custom_download_in_progress.cd_download_state
import kotlinx.android.synthetic.main.layout_custom_download_in_progress.cd_eta
import kotlinx.android.synthetic.main.layout_custom_download_in_progress.cd_progress
import kotlinx.android.synthetic.main.layout_custom_download_required.cd_download_button
import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.downloader.model.DownloadItem
import org.kiwix.kiwixmobile.core.extensions.setDistinctDisplayedChild
import org.kiwix.kiwixmobile.core.extensions.viewModel
import org.kiwix.kiwixmobile.custom.R
import org.kiwix.kiwixmobile.custom.customActivityComponent
import org.kiwix.kiwixmobile.custom.download.Action.ClickedDownload
import org.kiwix.kiwixmobile.custom.download.Action.ClickedRetry
import org.kiwix.kiwixmobile.custom.download.State.DownloadComplete
import org.kiwix.kiwixmobile.custom.download.State.DownloadFailed
import org.kiwix.kiwixmobile.custom.download.State.DownloadInProgress
import org.kiwix.kiwixmobile.custom.download.State.DownloadRequired
import javax.inject.Inject
class CustomDownloadActivity : BaseActivity() {
private val downloadViewModel by lazy {
viewModel<CustomDownloadViewModel>(viewModelFactory)
}
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
private val compositeDisposable = CompositeDisposable()
override fun injection() {
customActivityComponent.inject(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom_download)
downloadViewModel.state.observe(this, Observer(::render))
compositeDisposable.add(
downloadViewModel.effects.subscribe(
{ it.invokeWith(this) },
Throwable::printStackTrace
)
)
cd_download_button.setOnClickListener { downloadViewModel.actions.offer(ClickedDownload) }
cd_retry_button.setOnClickListener { downloadViewModel.actions.offer(ClickedRetry) }
}
override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
}
private fun render(state: State) {
return when (state) {
DownloadRequired -> cd_view_animator.setDistinctDisplayedChild(0)
is DownloadInProgress -> {
cd_view_animator.setDistinctDisplayedChild(1)
render(state.downloads[0])
}
is DownloadFailed -> {
cd_view_animator.setDistinctDisplayedChild(2)
cd_error_text.text = state.downloadState.toReadableState(this)
}
DownloadComplete -> cd_view_animator.setDistinctDisplayedChild(3)
}
}
private fun render(downloadItem: DownloadItem) {
cd_progress.progress = downloadItem.progress
cd_eta.text = downloadItem.readableEta
cd_download_state.text = downloadItem.downloadState.toReadableState(this)
}
}

View File

@ -0,0 +1,96 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.custom.download
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.processors.PublishProcessor
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.dao.FetchDownloadDao
import org.kiwix.kiwixmobile.core.downloader.model.DownloadItem
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState.Failed
import org.kiwix.kiwixmobile.custom.download.Action.ClickedDownload
import org.kiwix.kiwixmobile.custom.download.Action.ClickedRetry
import org.kiwix.kiwixmobile.custom.download.Action.DatabaseEmission
import org.kiwix.kiwixmobile.custom.download.State.DownloadComplete
import org.kiwix.kiwixmobile.custom.download.State.DownloadFailed
import org.kiwix.kiwixmobile.custom.download.State.DownloadInProgress
import org.kiwix.kiwixmobile.custom.download.State.DownloadRequired
import org.kiwix.kiwixmobile.custom.download.effects.DownloadCustom
import org.kiwix.kiwixmobile.custom.download.effects.FinishAndStartMain
import org.kiwix.kiwixmobile.custom.download.effects.SetPreferredStorageWithMostSpace
import javax.inject.Inject
class CustomDownloadViewModel @Inject constructor(
downloadDao: FetchDownloadDao,
setPreferredStorageWithMostSpace: SetPreferredStorageWithMostSpace,
private val downloadCustom: DownloadCustom
) : ViewModel() {
val state = MutableLiveData<State>().apply { value = DownloadRequired }
val actions = PublishProcessor.create<Action>()
private val _effects = PublishProcessor.create<SideEffect<*>>()
val effects = _effects.startWith(setPreferredStorageWithMostSpace)
private val compositeDisposable = CompositeDisposable()
init {
compositeDisposable.addAll(
reducer(),
downloadsAsActions(downloadDao)
)
}
private fun reducer() = actions.map { reduce(it, state.value!!) }
.distinctUntilChanged()
.subscribe(state::postValue, Throwable::printStackTrace)
private fun downloadsAsActions(downloadDao: FetchDownloadDao) =
downloadDao.downloads()
.map { it.map(::DownloadItem) }
.subscribe(
{ actions.offer(DatabaseEmission(it)) },
Throwable::printStackTrace
)
private fun reduce(action: Action, state: State): State {
return when (action) {
is DatabaseEmission -> reduceDatabaseEmission(state, action)
ClickedRetry,
ClickedDownload -> state.also { _effects.offer(downloadCustom) }
}
}
private fun reduceDatabaseEmission(state: State, action: DatabaseEmission) = when (state) {
is DownloadFailed,
DownloadRequired ->
if (action.downloads.isNotEmpty()) DownloadInProgress(action.downloads)
else state
is DownloadInProgress ->
if (action.downloads.isNotEmpty())
if (action.downloads[0].downloadState is Failed)
DownloadFailed(action.downloads[0].downloadState)
else
DownloadInProgress(action.downloads)
else
DownloadComplete.also { _effects.offer(FinishAndStartMain()) }
DownloadComplete -> state
}
}

View File

@ -0,0 +1,29 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.custom.download
import org.kiwix.kiwixmobile.core.downloader.model.DownloadItem
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState
sealed class State {
object DownloadRequired : State()
data class DownloadInProgress(val downloads: List<DownloadItem>) : State()
data class DownloadFailed(val downloadState: DownloadState) : State()
object DownloadComplete : State()
}

View File

@ -0,0 +1,63 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.custom.download.effects
import android.app.Activity
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.downloader.Downloader
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.custom.BuildConfig
import javax.inject.Inject
data class DownloadCustom @Inject constructor(val downloader: Downloader) : SideEffect<Unit> {
override fun invokeWith(activity: Activity) {
downloader.download(emptyBook(id = "custom", url = BuildConfig.ZIM_URL))
}
private fun emptyBook(
id: String = "",
title: String = "",
description: String = "",
language: String = "",
creator: String = "",
publisher: String = "",
date: String = "",
url: String = "",
articleCount: String = "",
mediaCount: String = "",
size: String = "",
name: String = "",
favIcon: String = ""
) =
Book().apply {
this.id = id
this.title = title
this.description = description
this.language = language
this.creator = creator
this.publisher = publisher
this.date = date
this.url = url
this.articleCount = articleCount
this.mediaCount = mediaCount
this.size = size
bookName = name
favicon = favIcon
}
}

View File

@ -0,0 +1,31 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.custom.download.effects
import android.app.Activity
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.extensions.start
import org.kiwix.kiwixmobile.custom.main.CustomMainActivity
class FinishAndStartMain() : SideEffect<Unit> {
override fun invokeWith(activity: Activity) {
activity.finish()
activity.start<CustomMainActivity>()
}
}

View File

@ -0,0 +1,38 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.custom.download.effects
import android.app.Activity
import androidx.core.content.ContextCompat
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.settings.StorageCalculator
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import javax.inject.Inject
class SetPreferredStorageWithMostSpace @Inject constructor(
private val storageCalculator: StorageCalculator,
private val sharedPreferenceUtil: SharedPreferenceUtil
) : SideEffect<Unit> {
override fun invokeWith(activity: Activity) {
ContextCompat.getExternalFilesDirs(activity, null)
.filterNotNull()
.maxBy(storageCalculator::availableBytes)
?.let { sharedPreferenceUtil.putPrefStorage(it.path) }
}
}

View File

@ -0,0 +1,65 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.custom.main
import android.content.Context
import androidx.core.content.ContextCompat
import org.kiwix.kiwixmobile.custom.main.ValidationState.HasBothFiles
import org.kiwix.kiwixmobile.custom.main.ValidationState.HasFile
import org.kiwix.kiwixmobile.custom.main.ValidationState.HasNothing
import java.io.File
import javax.inject.Inject
class CustomFileValidator @Inject constructor(private val context: Context) {
fun validate(onFilesFound: (ValidationState) -> Unit, onNoFilesFound: () -> Unit) =
when (val installationState = detectInstallationState()) {
is HasBothFiles,
is HasFile -> onFilesFound(installationState)
HasNothing -> onNoFilesFound()
}
private fun detectInstallationState(
obbFiles: List<File> = obbFiles(),
zimFiles: List<File> = zimFiles()
): ValidationState {
return when {
obbFiles.isNotEmpty() && zimFiles().isNotEmpty() -> HasBothFiles(obbFiles[0], zimFiles[0])
obbFiles.isNotEmpty() -> HasFile(obbFiles[0])
zimFiles.isNotEmpty() -> HasFile(zimFiles[0])
else -> HasNothing
}
}
private fun obbFiles() = scanDirs(ContextCompat.getObbDirs(context), "obb")
private fun zimFiles() =
scanDirs(ContextCompat.getExternalFilesDirs(context, null), "zim")
private fun scanDirs(dirs: Array<out File>, extensionToMatch: String): List<File> =
dirs.fold(listOf()) { acc, dir ->
acc + dir.walk().filter { it.extension.startsWith(extensionToMatch) }.toList()
}
}
sealed class ValidationState {
data class HasBothFiles(val obbFile: File, val zimFile: File) : ValidationState()
data class HasFile(val file: File) : ValidationState()
object HasNothing : ValidationState()
}

View File

@ -22,23 +22,24 @@ import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Menu
import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri
import org.kiwix.kiwixmobile.core.CoreApp
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.extensions.start
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.main.WebViewCallback
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.utils.Constants.TAG_KIWIX
import org.kiwix.kiwixmobile.core.utils.LanguageUtils
import org.kiwix.kiwixmobile.core.utils.StyleUtils.dialogStyle
import org.kiwix.kiwixmobile.core.utils.files.FileUtils
import org.kiwix.kiwixmobile.custom.BuildConfig
import org.kiwix.kiwixmobile.custom.customActivityComponent
import org.kiwix.kiwixmobile.custom.download.CustomDownloadActivity
import org.kiwix.kiwixmobile.custom.main.ValidationState.HasBothFiles
import org.kiwix.kiwixmobile.custom.main.ValidationState.HasFile
import java.io.File
import java.util.Locale
import javax.inject.Inject
class CustomMainActivity : CoreMainActivity() {
@Inject lateinit var customFileValidator: CustomFileValidator
override fun showHomePage() {
Log.e("CustomMain", "tried to show home page")
@ -54,17 +55,22 @@ class CustomMainActivity : CoreMainActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG_KIWIX, "This is a custom app:$packageName")
if (loadCustomAppContent()) {
Log.d(TAG_KIWIX, "Found custom content, continuing...")
// Continue
} else {
Log.e(TAG_KIWIX, "Problem finding the content, no more OnCreate() code")
// What should we do here? exit? I'll investigate empirically.
// It seems unpredictable behaviour if the code returns at this point as yesterday
// it didn't crash yet today the app crashes because it tries to load books
// in onResume();
}
requireEnforcedLanguage()
customFileValidator.validate(
{
when (it) {
is HasFile -> openZimFile(it.file)
is HasBothFiles -> {
it.zimFile.delete()
openZimFile(it.obbFile)
}
}
},
{
finish()
start<CustomDownloadActivity>()
}
)
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
@ -79,74 +85,15 @@ class CustomMainActivity : CoreMainActivity() {
zimReaderContainer: ZimReaderContainer
) = CustomWebViewClient(webViewCallback, zimReaderContainer)
/**
* loadCustomAppContent Return true if all's well, else false.
*/
private fun loadCustomAppContent(): Boolean {
Log.d(
TAG_KIWIX,
"Kiwix Custom App starting for the first time. Checking Companion ZIM: " +
BuildConfig.ZIM_FILE_NAME
)
private fun requireEnforcedLanguage(): Boolean {
val currentLocaleCode = Locale.getDefault().toString()
// Custom App recommends to start off a specific language
if (BuildConfig.ENFORCED_LANG.length > 0 && BuildConfig.ENFORCED_LANG != currentLocaleCode) {
// change the locale machinery
if (BuildConfig.ENFORCED_LANG.isNotEmpty() && BuildConfig.ENFORCED_LANG != currentLocaleCode) {
LanguageUtils.handleLocaleChange(this, BuildConfig.ENFORCED_LANG)
// save new locale into preferences for next startup
sharedPreferenceUtil.putPrefLanguage(BuildConfig.ENFORCED_LANG)
// restart activity for new locale to take effect
this.setResult(1236)
this.finish()
this.startActivity(Intent(this, this.javaClass))
return false
}
var filePath = ""
if (BuildConfig.HAS_EMBEDDED_ZIM) {
val appPath = packageResourcePath
val libDir = File(appPath.substring(0, appPath.lastIndexOf("/")) + "/lib/")
if (libDir.exists() && libDir.listFiles().size > 0) {
filePath = libDir.listFiles()[0].path + "/" + BuildConfig.ZIM_FILE_NAME
}
if (filePath.isEmpty() || !File(filePath).exists()) {
filePath = String.format(
"/data/data/%s/lib/%s", packageName,
BuildConfig.ZIM_FILE_NAME
)
}
} else {
filePath = generateExpansionFilePath()
}
Log.d(TAG_KIWIX, "BuildConfig.ZIM_FILE_SIZE = " + BuildConfig.ZIM_FILE_SIZE)
if (!FileUtils.doesFileExist(filePath, BuildConfig.ZIM_FILE_SIZE, false)) {
val zimFileMissingBuilder = AlertDialog.Builder(this, dialogStyle())
zimFileMissingBuilder.setTitle(R.string.app_name)
zimFileMissingBuilder.setMessage(R.string.custom_app_missing_content)
zimFileMissingBuilder.setIcon(R.mipmap.kiwix_icon)
val activity = this
zimFileMissingBuilder.setPositiveButton(
getString(R.string.go_to_play_store)
) { dialog, which ->
startActivity(Intent(Intent.ACTION_VIEW).apply {
data = "market://details?id=$packageName".toUri()
})
activity.finish()
}
zimFileMissingBuilder.setCancelable(false)
val zimFileMissingDialog = zimFileMissingBuilder.create()
zimFileMissingDialog.show()
return false
} else {
openZimFile(File(filePath))
startActivity(Intent(this, this.javaClass))
return true
}
return false
}
override fun manageZimFiles(tab: Int) {

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<ImageView
android:id="@+id/cd_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:src="@drawable/kiwix_icon_with_title"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ViewAnimator
android:id="@+id/cd_view_animator"
android:layout_width="0dp"
android:layout_height="0dp"
android:inAnimation="@anim/fade_in"
android:outAnimation="@anim/fade_out"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/cd_icon">
<include layout="@layout/layout_custom_download_required" />
<include layout="@layout/layout_custom_download_in_progress" />
<include layout="@layout/layout_custom_download_error" />
<include layout="@layout/layout_custom_download_complete" />
</ViewAnimator>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:showIn="@layout/activity_custom_download">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/complete"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias=".3" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:showIn="@layout/activity_custom_download">
<TextView
android:id="@+id/cd_error_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias=".3" />
<Button
android:id="@+id/cd_retry_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/retry"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/cd_error_text" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/cd_download_state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/cd_progress"
app:layout_constraintLeft_toLeftOf="@+id/cd_progress"
app:layout_constraintRight_toRightOf="@+id/cd_progress" />
<ProgressBar
android:id="@+id/cd_progress"
style="?android:progressBarStyleHorizontal"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:max="100"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias=".3" />
<TextView
android:id="@+id/cd_eta"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginLeft="5dp"
app:layout_constraintBottom_toBottomOf="@id/cd_progress"
app:layout_constraintStart_toEndOf="@id/cd_progress"
app:layout_constraintTop_toTopOf="@id/cd_progress" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:showIn="@layout/activity_custom_download">
<TextView
android:id="@+id/cd_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/invalid_installation"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias=".3" />
<Button
android:id="@+id/cd_download_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="@string/download"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/cd_text" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="retry">Retry</string>
<string name="download">Download</string>
<string name="invalid_installation">Invalid Install. Please Download Zim.\n Ensure WiFi is on and you have enough storage</string>
<string name="complete">Complete</string>
</resources>