diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/OnlineLibraryFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/OnlineLibraryFragment.kt index 1241f61c3..f26a4a64d 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/OnlineLibraryFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/OnlineLibraryFragment.kt @@ -195,6 +195,13 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions { showInternetAccessViaMobileNetworkDialog() } } + zimManageViewModel.downloadProgress.observe(viewLifecycleOwner) { progress -> + fragmentDestinationDownloadBinding?.onlineLibraryProgressBar?.progress = progress + } + + zimManageViewModel.downloadStatus.observe(viewLifecycleOwner) { status -> + fragmentDestinationDownloadBinding?.libraryErrorText?.text = status + } setupMenu() // hides keyboard when scrolled @@ -268,6 +275,7 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions { libraryErrorText.visibility = View.GONE libraryList.visibility = View.VISIBLE } + showProgressBarOfFetchingOnlineLibrary() } private fun hideRecyclerviewAndShowSwipeDownForLibraryErrorText() { @@ -277,6 +285,17 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions { ) libraryErrorText.visibility = View.VISIBLE libraryList.visibility = View.GONE + onlineLibraryProgressBar.visibility = View.GONE + } + } + + private fun showProgressBarOfFetchingOnlineLibrary() { + fragmentDestinationDownloadBinding?.apply { + onlineLibraryProgressBar.visibility = View.VISIBLE + libraryErrorText.apply { + visibility = View.VISIBLE + setText(string.reaching_remote_library) + } } } @@ -345,6 +364,7 @@ class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions { if (it != null) { libraryAdapter.items = it } + fragmentDestinationDownloadBinding?.onlineLibraryProgressBar?.visibility = View.GONE if (it?.isEmpty() == true) { fragmentDestinationDownloadBinding?.libraryErrorText?.setText( if (isNotConnected) string.no_network_connection diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/AppProgressListenerProvider.kt b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/AppProgressListenerProvider.kt new file mode 100644 index 000000000..3e7b588b3 --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/AppProgressListenerProvider.kt @@ -0,0 +1,31 @@ +/* + * Kiwix Android + * Copyright (c) 2024 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.zimManager + +import org.kiwix.kiwixmobile.core.data.remote.OnlineLibraryProgressListener + +class AppProgressListenerProvider( + private val zimManageViewModel: ZimManageViewModel +) : OnlineLibraryProgressListener { + @Suppress("MagicNumber") + override fun onProgress(bytesRead: Long, contentLength: Long, done: Boolean) { + val progress = if (contentLength == -1L) 0 else (bytesRead * 100 / contentLength).toInt() + zimManageViewModel.downloadProgress.postValue(progress) + } +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt index 556178c01..0ec056b3f 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zimManager/ZimManageViewModel.kt @@ -33,6 +33,11 @@ import io.reactivex.plugins.RxJavaPlugins import io.reactivex.processors.BehaviorProcessor import io.reactivex.processors.PublishProcessor import io.reactivex.schedulers.Schedulers +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import okhttp3.logging.HttpLoggingInterceptor.Level.BASIC +import okhttp3.logging.HttpLoggingInterceptor.Level.NONE +import org.kiwix.kiwixmobile.BuildConfig.DEBUG import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.StorageObserver import org.kiwix.kiwixmobile.core.base.SideEffect @@ -42,6 +47,13 @@ import org.kiwix.kiwixmobile.core.dao.NewBookDao import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao import org.kiwix.kiwixmobile.core.data.DataSource import org.kiwix.kiwixmobile.core.data.remote.KiwixService +import org.kiwix.kiwixmobile.core.data.remote.ProgressResponseBody +import org.kiwix.kiwixmobile.core.data.remote.UserAgentInterceptor +import org.kiwix.kiwixmobile.core.di.modules.CALL_TIMEOUT +import org.kiwix.kiwixmobile.core.di.modules.CONNECTION_TIMEOUT +import org.kiwix.kiwixmobile.core.di.modules.KIWIX_DOWNLOAD_URL +import org.kiwix.kiwixmobile.core.di.modules.READ_TIMEOUT +import org.kiwix.kiwixmobile.core.di.modules.USER_AGENT import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book @@ -81,6 +93,7 @@ import java.io.IOException import java.util.LinkedList import java.util.Locale import java.util.concurrent.TimeUnit.MILLISECONDS +import java.util.concurrent.TimeUnit.SECONDS import javax.inject.Inject const val DEFAULT_PROGRESS = 0 @@ -92,7 +105,7 @@ class ZimManageViewModel @Inject constructor( private val bookDao: NewBookDao, private val languageDao: NewLanguagesDao, private val storageObserver: StorageObserver, - private val kiwixService: KiwixService, + private var kiwixService: KiwixService, private val context: Application, private val connectivityBroadcastReceiver: ConnectivityBroadcastReceiver, private val bookUtils: BookUtils, @@ -127,12 +140,44 @@ class ZimManageViewModel @Inject constructor( val requestFiltering = BehaviorProcessor.createDefault("") private var compositeDisposable: CompositeDisposable? = CompositeDisposable() + val downloadProgress = MutableLiveData() + val downloadStatus = MutableLiveData() init { + // add listener to retrofit to get updates of downloading online library + kiwixService = createKiwixServiceWithProgressListener() compositeDisposable?.addAll(*disposables()) context.registerReceiver(connectivityBroadcastReceiver) } + private fun createKiwixServiceWithProgressListener(): KiwixService { + val customOkHttpClient = OkHttpClient().newBuilder() + .followRedirects(true) + .followSslRedirects(true) + .connectTimeout(CONNECTION_TIMEOUT, SECONDS) + .readTimeout(READ_TIMEOUT, SECONDS) + .callTimeout(CALL_TIMEOUT, SECONDS) + .addNetworkInterceptor( + HttpLoggingInterceptor().apply { + level = if (DEBUG) BASIC else NONE + } + ) + .addNetworkInterceptor(UserAgentInterceptor(USER_AGENT)) + .addNetworkInterceptor { chain -> + val originalResponse = chain.proceed(chain.request()) + originalResponse.newBuilder() + .body( + ProgressResponseBody( + originalResponse.body!!, + AppProgressListenerProvider(this) + ) + ) + .build() + } + .build() + return KiwixService.ServiceCreator.newHackListService(customOkHttpClient, KIWIX_DOWNLOAD_URL) + } + @VisibleForTesting fun onClearedExposed() { onCleared() @@ -232,7 +277,7 @@ class ZimManageViewModel @Inject constructor( } private fun requestsAndConnectivtyChangesToLibraryRequests( - library: PublishProcessor + library: PublishProcessor, ) = Flowable.combineLatest( requestDownloadLibrary, @@ -254,21 +299,37 @@ class ZimManageViewModel @Inject constructor( .map { } } } + .doOnEach { + downloadStatus.postValue("Reaching remote library") + } + .switchMap { + Flowable.fromCallable { + downloadStatus.postValue("Starting download of the online library") + } + } .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) - .subscribe( - { - compositeDisposable?.add( - kiwixService.library - .retry(5) - .subscribe(library::onNext) { - it.printStackTrace() - library.onNext(LibraryNetworkEntity().apply { book = LinkedList() }) - } - ) - }, - Throwable::printStackTrace - ) + .flatMap { + kiwixService.library + .toFlowable() + .retry(5) + .doOnSubscribe { + downloadStatus.postValue("Downloading library 0%") + } + .map { response -> + downloadStatus.postValue("Downloading library... parsing response") + response + } + .doFinally { + downloadStatus.postValue("Remote library downloaded, parsing data") + } + .onErrorReturn { + it.printStackTrace() + downloadStatus.postValue("Failed to download the library") + LibraryNetworkEntity().apply { book = LinkedList() } + } + } + .subscribe(library::onNext, Throwable::printStackTrace) private fun updateNetworkStates() = connectivityBroadcastReceiver.networkStates.subscribe( diff --git a/app/src/main/res/layout/fragment_destination_download.xml b/app/src/main/res/layout/fragment_destination_download.xml index 15ed6b35e..194c268d4 100644 --- a/app/src/main/res/layout/fragment_destination_download.xml +++ b/app/src/main/res/layout/fragment_destination_download.xml @@ -66,4 +66,13 @@ app:layout_constraintVertical_bias="0.45" tools:ignore="RequiredSize" /> + + diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/OnlineLibraryProgressListener.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/OnlineLibraryProgressListener.kt new file mode 100644 index 000000000..dd2301aec --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/OnlineLibraryProgressListener.kt @@ -0,0 +1,23 @@ +/* + * Kiwix Android + * Copyright (c) 2024 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.core.data.remote + +interface OnlineLibraryProgressListener { + fun onProgress(bytesRead: Long, contentLength: Long, done: Boolean) +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/ProgressResponseBody.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/ProgressResponseBody.kt new file mode 100644 index 000000000..ef05bc8b4 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/data/remote/ProgressResponseBody.kt @@ -0,0 +1,72 @@ +/* + * Kiwix Android + * Copyright (c) 2024 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.core.data.remote + +import okhttp3.MediaType +import okhttp3.ResponseBody +import okio.Buffer +import okio.BufferedSource +import okio.ForwardingSource +import okio.Source +import okio.buffer +import org.kiwix.kiwixmobile.core.utils.files.Log + +class ProgressResponseBody( + private val responseBody: ResponseBody, + private val progressListener: OnlineLibraryProgressListener +) : ResponseBody() { + + private var bufferedSource: BufferedSource? = null + + override fun contentType(): MediaType? = responseBody.contentType() + + override fun contentLength(): Long = responseBody.contentLength() + + @Suppress("UnsafeCallOnNullableType") + override fun source(): BufferedSource { + if (bufferedSource == null) { + bufferedSource = source(responseBody.source()).buffer() + } + return bufferedSource!! + } + + private fun source(source: Source): Source { + return object : ForwardingSource(source) { + var totalBytesRead = 0L + override fun read(sink: Buffer, byteCount: Long): Long { + val bytesRead = super.read(sink, byteCount) + totalBytesRead += if (bytesRead != -1L) bytesRead else 0 + val isDone = bytesRead == -1L + progressListener.onProgress( + totalBytesRead, + responseBody.contentLength(), + isDone + ).also { + Log.e( + "PROGRESS", + "onProgress: ${contentLength()} and byteRead = $totalBytesRead\n" + + " sink ${bytesRead == -1L} \n byteRead = $bytesRead " + + "\n bufferedSource = ${bufferedSource?.isOpen}" + ) + } + return bytesRead + } + } + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/NetworkModule.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/NetworkModule.kt index 0e4cefa35..aafb0e93c 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/NetworkModule.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/NetworkModule.kt @@ -30,11 +30,11 @@ import org.kiwix.kiwixmobile.core.data.remote.UserAgentInterceptor import java.util.concurrent.TimeUnit.SECONDS import javax.inject.Singleton -private const val CONNECTION_TIMEOUT = 10L -private const val READ_TIMEOUT = 60L -private const val CALL_TIMEOUT = 60L -private const val USER_AGENT = "kiwix-android-version:${BuildConfig.VERSION_CODE}" -private const val KIWIX_DOWNLOAD_URL = "https://mirror.download.kiwix.org/" +const val CONNECTION_TIMEOUT = 10L +const val READ_TIMEOUT = 60L +const val CALL_TIMEOUT = 60L +const val USER_AGENT = "kiwix-android-version:${BuildConfig.VERSION_CODE}" +const val KIWIX_DOWNLOAD_URL = "https://mirror.download.kiwix.org/" @Module class NetworkModule { diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 176cbb597..3f856f264 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -348,6 +348,7 @@ System unable to grant permission! Allow Swipe Down for Library + Reaching remote library @string/on @string/off