merge with develop

This commit is contained in:
HissPirat 2020-09-13 13:13:32 +02:00
commit 1385baf348
58 changed files with 1717 additions and 1731 deletions

View File

@ -52,7 +52,7 @@ Branch names should be in the format **\<issue-number\>-kebab-case-title**
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.
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).
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 adhere 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).
### Design and style

View File

@ -38,10 +38,10 @@
<ID>PackageNaming:ShareFiles.kt$package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects</ID>
<ID>PackageNaming:SimplePageChangeListener.kt$package org.kiwix.kiwixmobile.zim_manager</ID>
<ID>PackageNaming:StartMultiSelection.kt$package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects</ID>
<ID>PackageNaming:WifiDirectManager.kt$package org.kiwix.kiwixmobile.local_file_transfer</ID>
<ID>PackageNaming:WifiP2pDelegate.kt$package org.kiwix.kiwixmobile.local_file_transfer.adapter</ID>
<ID>PackageNaming:WifiP2pViewHolder.kt$package org.kiwix.kiwixmobile.local_file_transfer.adapter</ID>
<ID>PackageNaming:WifiPeerListAdapter.kt$package org.kiwix.kiwixmobile.local_file_transfer.adapter</ID>
<ID>PackageNaming:WifiDirectManager.kt$package org.kiwix.kiwixmobile.localFileTransfer</ID>
<ID>PackageNaming:WifiP2pDelegate.kt$package org.kiwix.kiwixmobile.localFileTransfer.adapter</ID>
<ID>PackageNaming:WifiP2pViewHolder.kt$package org.kiwix.kiwixmobile.localFileTransfer.adapter</ID>
<ID>PackageNaming:WifiPeerListAdapter.kt$package org.kiwix.kiwixmobile.localFileTransfer.adapter</ID>
<ID>PackageNaming:ZimFileSelectFragment.kt$package org.kiwix.kiwixmobile.zim_manager.fileselect_view</ID>
<ID>PackageNaming:ZimManageActivity.kt$package org.kiwix.kiwixmobile.zim_manager</ID>
<ID>PackageNaming:ZimManageViewModel.kt$package org.kiwix.kiwixmobile.zim_manager</ID>

View File

@ -0,0 +1,133 @@
/*
* Kiwix Android
* Copyright (c) 2020 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.settings
import androidx.annotation.StringRes
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.espresso.matcher.ViewMatchers.withText
import applyWithViewHierarchyPrinting
import com.schibsted.spain.barista.assertion.BaristaVisibilityAssertions.assertDisplayed
import org.hamcrest.Matchers
import org.kiwix.kiwixmobile.BaseRobot
import org.kiwix.kiwixmobile.Findable.Text
import org.kiwix.kiwixmobile.R
/**
* Authored by Ayush Shrivastava on 25/8/20
*/
fun settingsRobo(func: SettingsRobot.() -> Unit) =
SettingsRobot().applyWithViewHierarchyPrinting(func)
class SettingsRobot : BaseRobot() {
init {
assertDisplayed(R.string.menu_settings)
}
private fun clickRecyclerViewItems(@StringRes vararg stringIds: Int) {
onView(
withClassName(Matchers.`is`(RecyclerView::class.java.name))
).perform(
actionOnItem<RecyclerView.ViewHolder>(
hasDescendant(Matchers.anyOf(*stringIds.matchers())), ViewActions.click()
)
)
}
fun toggleBackToTopPref() {
clickRecyclerViewItems(R.string.pref_back_to_top)
}
fun toggleOpenNewTabInBackground() {
clickRecyclerViewItems(R.string.pref_newtab_background_title)
}
fun toggleExternalLinkWarningPref() {
clickRecyclerViewItems(R.string.pref_external_link_popup_title)
}
fun toggleWifiDownloadsOnlyPref() {
clickRecyclerViewItems(R.string.pref_wifi_only)
}
fun clickLanguagePreference() {
clickRecyclerViewItems(R.string.device_default)
}
fun assertLanguagePrefDialogDisplayed() {
assertDisplayed(R.string.pref_language_title)
}
fun clickStoragePreference() {
clickRecyclerViewItems(R.string.internal_storage, R.string.external_storage)
}
fun assertStorageDialogDisplayed() {
assertDisplayed(R.string.pref_storage)
}
fun clickClearHistoryPreference() {
clickRecyclerViewItems(R.string.pref_clear_all_history_title)
}
fun assertHistoryDialogDisplayed() {
assertDisplayed(R.string.clear_all_history_dialog_title)
}
fun clickNightModePreference() {
clickRecyclerViewItems(R.string.pref_night_mode)
}
fun assertNightModeDialogDisplayed() {
for (nightModeString in nightModeStrings()) {
assertDisplayed(nightModeString)
}
}
fun clickCredits() {
clickRecyclerViewItems(R.string.pref_credits_title)
}
fun assertContributorsDialogDisplayed() {
isVisible(Text("OK"))
}
fun assertZoomTextViewPresent() {
clickRecyclerViewItems(R.string.pref_text_zoom_title)
}
fun assertVersionTextViewPresent() {
clickRecyclerViewItems(R.string.pref_info_version)
}
fun dismissDialog() {
pressBack()
}
private fun nightModeStrings(): Array<String> =
context.resources.getStringArray(R.array.pref_night_modes_entries)
private fun IntArray.matchers() = map(::withText).toTypedArray()
}

View File

@ -133,7 +133,7 @@
</intent-filter>
</activity>
<activity
android:name=".local_file_transfer.LocalFileTransferActivity"
android:name=".localFileTransfer.LocalFileTransferActivity"
android:label="Send to nearby device"
android:screenOrientation="portrait">
<intent-filter>

View File

@ -26,7 +26,7 @@ import org.kiwix.kiwixmobile.di.modules.KiwixActivityModule
import org.kiwix.kiwixmobile.intro.IntroFragment
import org.kiwix.kiwixmobile.intro.IntroModule
import org.kiwix.kiwixmobile.language.LanguageFragment
import org.kiwix.kiwixmobile.local_file_transfer.LocalFileTransferActivity
import org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferActivity
import org.kiwix.kiwixmobile.main.KiwixMainActivity
import org.kiwix.kiwixmobile.nav.destination.library.LocalLibraryFragment
import org.kiwix.kiwixmobile.nav.destination.library.OnlineLibraryFragment
@ -35,7 +35,6 @@ import org.kiwix.kiwixmobile.settings.KiwixSettingsFragment
import org.kiwix.kiwixmobile.webserver.ZimHostFragment
import org.kiwix.kiwixmobile.webserver.ZimHostModule
import org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects.DeleteFiles
import org.kiwix.kiwixmobile.zim_manager.library_view.LibraryFragment
@ActivityScope
@Subcomponent(
@ -46,7 +45,6 @@ import org.kiwix.kiwixmobile.zim_manager.library_view.LibraryFragment
]
)
interface KiwixActivityComponent : CoreActivityComponent {
fun inject(libraryFragment: LibraryFragment)
fun inject(readerFragment: KiwixReaderFragment)
fun inject(localLibraryFragment: LocalLibraryFragment)
fun inject(deleteFiles: DeleteFiles)

View File

@ -18,10 +18,10 @@
@file:Suppress("PackageNaming")
package org.kiwix.kiwixmobile.local_file_transfer
package org.kiwix.kiwixmobile.localFileTransfer
import android.net.Uri
import org.kiwix.kiwixmobile.local_file_transfer.WifiDirectManager.Companion.getFileName
import org.kiwix.kiwixmobile.localFileTransfer.WifiDirectManager.Companion.getFileName
/**
* Helper class, part of the local file sharing module.

View File

@ -17,7 +17,7 @@
*/
@file:Suppress("PackageNaming")
package org.kiwix.kiwixmobile.local_file_transfer
package org.kiwix.kiwixmobile.localFileTransfer
import android.view.View
import android.view.ViewGroup
@ -26,21 +26,20 @@ import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item_transfer_list.image_view_file_transferred
import kotlinx.android.synthetic.main.item_transfer_list.progress_bar_transferring_file
import kotlinx.android.synthetic.main.item_transfer_list.text_view_file_item_name
import org.kiwix.kiwixmobile.local_file_transfer.FileItem.FileStatus.SENDING
import org.kiwix.kiwixmobile.local_file_transfer.FileItem.FileStatus.SENT
import org.kiwix.kiwixmobile.local_file_transfer.FileItem.FileStatus.ERROR
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.base.adapter.BaseViewHolder
import org.kiwix.kiwixmobile.core.extensions.ViewGroupExtensions.inflate
import org.kiwix.kiwixmobile.local_file_transfer.FileListAdapter.FileViewHolder
import java.util.ArrayList
import org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.ERROR
import org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.SENDING
import org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.SENT
import org.kiwix.kiwixmobile.localFileTransfer.FileListAdapter.FileViewHolder
/**
* Helper class, part of the local file sharing module.
*
* Defines the Adapter for the list of file-items displayed in {TransferProgressFragment}
*/
class FileListAdapter(private val fileItems: ArrayList<FileItem>) :
class FileListAdapter(private val fileItems: List<FileItem>) :
RecyclerView.Adapter<FileViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder =

View File

@ -18,7 +18,7 @@
@file:Suppress("PackageNaming")
package org.kiwix.kiwixmobile.local_file_transfer
package org.kiwix.kiwixmobile.localFileTransfer
import android.content.BroadcastReceiver
import android.content.Context

View File

@ -0,0 +1,378 @@
/*
* 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.localFileTransfer
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.location.LocationManager
import android.net.Uri
import android.net.wifi.p2p.WifiP2pDevice
import android.net.wifi.p2p.WifiP2pDeviceList
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_local_file_transfer.list_peer_devices
import kotlinx.android.synthetic.main.activity_local_file_transfer.progress_bar_searching_peers
import kotlinx.android.synthetic.main.activity_local_file_transfer.recycler_view_transfer_files
import kotlinx.android.synthetic.main.activity_local_file_transfer.text_view_device_name
import kotlinx.android.synthetic.main.activity_local_file_transfer.text_view_empty_peer_list
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.di.components.CoreComponent
import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog
import org.kiwix.kiwixmobile.kiwixActivityComponent
import org.kiwix.kiwixmobile.localFileTransfer.WifiDirectManager.Companion.getDeviceStatus
import org.kiwix.kiwixmobile.localFileTransfer.adapter.WifiP2pDelegate
import org.kiwix.kiwixmobile.localFileTransfer.adapter.WifiPeerListAdapter
import java.util.ArrayList
import javax.inject.Inject
/**
* Created by @Aditya-Sood as a part of GSoC 2019.
*
* This activity is the starting point for the module used for sharing zims between devices.
*
* The module is used for transferring ZIM files from one device to another, from within the
* app. Two devices are connected to each other using WiFi Direct, followed by file transfer.
*
* File transfer involves two phases:
* 1) Handshake with the selected peer device, using [PeerGroupHandshakeAsyncTask]
* 2) After handshake, starting the files transfer using [SenderDeviceAsyncTask] on the sender
* device and [ReceiverDeviceAsyncTask] files receiving device
*/
@SuppressLint("GoogleAppIndexingApiWarning", "Registered")
class LocalFileTransferActivity : BaseActivity(),
WifiDirectManager.Callbacks {
@Inject
lateinit var alertDialogShower: AlertDialogShower
@Inject
lateinit var wifiDirectManager: WifiDirectManager
@Inject
lateinit var locationManager: LocationManager
private var fileListAdapter: FileListAdapter? = null
private var wifiPeerListAdapter: WifiPeerListAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_local_file_transfer)
/*
* Presence of file Uris decides whether the device with the activity open is a sender or receiver:
* - On the sender device, this activity is started from the app chooser post selection
* of files to share in the Library
* - On the receiver device, the activity is started directly from within the 'Get Content'
* activity, without any file Uris
* */
val filesIntent = intent
val fileUriArrayList: ArrayList<Uri>? =
filesIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)
val fileForTransfer = fileUriArrayList?.map(::FileItem) ?: emptyList()
val toolbar: Toolbar =
findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp)
toolbar.setNavigationOnClickListener { finish() }
wifiPeerListAdapter = WifiPeerListAdapter(
WifiP2pDelegate(wifiDirectManager::sendToDevice)
)
list_peer_devices.adapter = wifiPeerListAdapter
list_peer_devices.layoutManager = LinearLayoutManager(this)
list_peer_devices.setHasFixedSize(true)
displayFileTransferProgress(fileForTransfer)
wifiDirectManager.startWifiDirectManager(fileForTransfer)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.wifi_file_share_items, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == R.id.menu_item_search_devices) {
/* Permissions essential for this module */
return when {
!checkCoarseLocationAccessPermission() ->
true
!checkExternalStorageWritePermission() ->
true
/* Initiate discovery */
!wifiDirectManager.isWifiP2pEnabled -> {
requestEnableWifiP2pServices()
true
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isLocationServiceEnabled -> {
requestEnableLocationServices()
true
}
else -> {
showPeerDiscoveryProgressBar()
wifiDirectManager.discoverPeerDevices()
true
}
}
}
return super.onOptionsItemSelected(item)
}
private fun showPeerDiscoveryProgressBar() { // Setup UI for searching peers
progress_bar_searching_peers.visibility = View.VISIBLE
list_peer_devices.visibility = View.INVISIBLE
text_view_empty_peer_list.visibility = View.INVISIBLE
}
/* From WifiDirectManager.Callbacks interface */
override fun onUserDeviceDetailsAvailable(userDevice: WifiP2pDevice?) {
// Update UI with user device's details
if (userDevice != null) {
text_view_device_name.text = userDevice.deviceName
Log.d(
TAG, getDeviceStatus(userDevice.status)
)
}
}
override fun onConnectionToPeersLost() {
wifiPeerListAdapter?.items = emptyList()
}
override fun onFilesForTransferAvailable(filesForTransfer: List<FileItem>) {
displayFileTransferProgress(filesForTransfer)
}
private fun displayFileTransferProgress(filesToSend: List<FileItem>) {
fileListAdapter = FileListAdapter(filesToSend)
recycler_view_transfer_files.adapter = fileListAdapter
recycler_view_transfer_files.layoutManager = LinearLayoutManager(this)
}
override fun onFileStatusChanged(itemIndex: Int) {
fileListAdapter?.notifyItemChanged(itemIndex)
}
override fun updateListOfAvailablePeers(peers: WifiP2pDeviceList) {
val deviceList: List<WifiP2pDevice> = ArrayList<WifiP2pDevice>(peers.deviceList)
progress_bar_searching_peers.visibility = View.GONE
list_peer_devices.visibility = View.VISIBLE
wifiPeerListAdapter?.items = deviceList
if (deviceList.isEmpty()) {
Log.d(TAG, "No devices found")
}
}
override fun onFileTransferComplete() {
finish()
}
/* Helper methods used for checking permissions and states of services */
private fun checkCoarseLocationAccessPermission(): Boolean {
// Required by Android to detect wifi-p2p peers
return if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
)
== PackageManager.PERMISSION_DENIED
) {
when {
ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.ACCESS_COARSE_LOCATION
) -> {
alertDialogShower.show(
KiwixDialog.LocationPermissionRationale,
{
ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
PERMISSION_REQUEST_CODE_COARSE_LOCATION
)
})
}
else -> {
ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION),
PERMISSION_REQUEST_CODE_COARSE_LOCATION
)
}
}
false
} else {
true
// Control reaches here: Either permission granted at install time, or at the time of request
}
}
private fun checkExternalStorageWritePermission(): Boolean { // To access and store the zims
return if (ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
== PackageManager.PERMISSION_DENIED
) {
if (ActivityCompat.shouldShowRequestPermissionRationale(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
) {
alertDialogShower.show(KiwixDialog.StoragePermissionRationale, {
ActivityCompat.requestPermissions(
this@LocalFileTransferActivity,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
PERMISSION_REQUEST_CODE_STORAGE_WRITE_ACCESS
)
})
} else {
ActivityCompat.requestPermissions(
this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
PERMISSION_REQUEST_CODE_STORAGE_WRITE_ACCESS
)
}
false
} else {
true
// Control reaches here: Either permission granted at install time, or at the time of request
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
if (grantResults[0] == PackageManager.PERMISSION_DENIED) {
when (requestCode) {
PERMISSION_REQUEST_CODE_COARSE_LOCATION -> {
Log.e(TAG, "Location permission not granted")
toast(
R.string.permission_refused_location,
Toast.LENGTH_SHORT
)
finish()
}
PERMISSION_REQUEST_CODE_STORAGE_WRITE_ACCESS -> {
Log.e(TAG, "Storage write permission not granted")
toast(
R.string.permission_refused_storage,
Toast.LENGTH_SHORT
)
finish()
}
else ->
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
}
private val isLocationServiceEnabled: Boolean
get() = isProviderEnabled(LocationManager.GPS_PROVIDER) ||
isProviderEnabled(LocationManager.NETWORK_PROVIDER)
private fun isProviderEnabled(locationProvider: String): Boolean {
return try {
locationManager.isProviderEnabled(locationProvider)
} catch (ex: SecurityException) {
ex.printStackTrace()
false
} catch (ex: IllegalArgumentException) {
ex.printStackTrace()
false
}
}
private fun requestEnableLocationServices() {
alertDialogShower.show(
KiwixDialog.EnableLocationServices, {
startActivityForResult(
Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS),
REQUEST_ENABLE_LOCATION_SERVICES
)
},
{
toast(
R.string.discovery_needs_location,
Toast.LENGTH_SHORT
)
}
)
}
private fun requestEnableWifiP2pServices() {
alertDialogShower.show(
KiwixDialog.EnableWifiP2pServices, {
startActivity(Intent(Settings.ACTION_WIFI_SETTINGS))
}, {
toast(
R.string.discovery_needs_wifi,
Toast.LENGTH_SHORT
)
}
)
}
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
when (requestCode) {
REQUEST_ENABLE_LOCATION_SERVICES -> {
if (!isLocationServiceEnabled) {
toast(
R.string.permission_refused_location,
Toast.LENGTH_SHORT
)
}
}
else ->
super.onActivityResult(requestCode, resultCode, data)
}
}
override fun onDestroy() {
wifiDirectManager.stopWifiDirectManager()
super.onDestroy()
}
override fun injection(coreComponent: CoreComponent) {
this.kiwixActivityComponent.inject(this)
}
companion object {
// Not a typo, 'Log' tags have a length upper limit of 25 characters
const val TAG = "LocalFileTransferActvty"
const val REQUEST_ENABLE_LOCATION_SERVICES = 1
private const val PERMISSION_REQUEST_CODE_COARSE_LOCATION = 1
private const val PERMISSION_REQUEST_CODE_STORAGE_WRITE_ACCESS = 2
}
}

View File

@ -16,7 +16,7 @@
*
*/
package org.kiwix.kiwixmobile.local_file_transfer;
package org.kiwix.kiwixmobile.localFileTransfer;
import android.os.AsyncTask;
import android.util.Log;
@ -29,6 +29,7 @@ import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import org.kiwix.kiwixmobile.core.BuildConfig;
/**
@ -119,7 +120,7 @@ class PeerGroupHandshakeAsyncTask extends AsyncTask<Void, Void, InetAddress> {
// Send total number of files which will be transferred
objectOutputStream.writeObject("" + wifiDirectManager.getTotalFilesForTransfer());
ArrayList<FileItem> fileItemArrayList = wifiDirectManager.getFilesForTransfer();
List<FileItem> fileItemArrayList = wifiDirectManager.getFilesForTransfer();
for (FileItem fileItem : fileItemArrayList) { // Send the names of each of those files, in order
objectOutputStream.writeObject(fileItem.getFileName());
Log.d(TAG, "Sending " + fileItem.getFileUri().toString());

View File

@ -16,7 +16,7 @@
*
*/
package org.kiwix.kiwixmobile.local_file_transfer;
package org.kiwix.kiwixmobile.localFileTransfer;
import android.os.AsyncTask;
import android.util.Log;
@ -27,12 +27,13 @@ import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import org.kiwix.kiwixmobile.core.BuildConfig;
import org.kiwix.kiwixmobile.core.R;
import static org.kiwix.kiwixmobile.local_file_transfer.FileItem.FileStatus;
import static org.kiwix.kiwixmobile.local_file_transfer.FileItem.FileStatus.ERROR;
import static org.kiwix.kiwixmobile.local_file_transfer.FileItem.FileStatus.SENDING;
import static org.kiwix.kiwixmobile.local_file_transfer.FileItem.FileStatus.SENT;
import static org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus;
import static org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.ERROR;
import static org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.SENDING;
import static org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.SENT;
/**
* Helper class for the local file sharing module.
@ -61,7 +62,7 @@ class ReceiverDeviceAsyncTask extends AsyncTask<Void, Integer, Boolean> {
Log.d(TAG, "Server: Socket opened at " + WifiDirectManager.FILE_TRANSFER_PORT);
final String zimStorageRootPath = wifiDirectManager.getZimStorageRootPath();
ArrayList<FileItem> fileItems = wifiDirectManager.getFilesForTransfer();
List<FileItem> fileItems = wifiDirectManager.getFilesForTransfer();
boolean isTransferErrorFree = true;
if (BuildConfig.DEBUG) Log.d(TAG, "Expecting " + fileItems.size() + " files");

View File

@ -16,7 +16,7 @@
*
*/
package org.kiwix.kiwixmobile.local_file_transfer;
package org.kiwix.kiwixmobile.localFileTransfer;
import android.app.Activity;
import android.content.ContentResolver;
@ -29,9 +29,9 @@ import java.net.InetSocketAddress;
import java.net.Socket;
import org.kiwix.kiwixmobile.core.BuildConfig;
import static org.kiwix.kiwixmobile.local_file_transfer.FileItem.FileStatus.ERROR;
import static org.kiwix.kiwixmobile.local_file_transfer.FileItem.FileStatus.SENDING;
import static org.kiwix.kiwixmobile.local_file_transfer.FileItem.FileStatus.SENT;
import static org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.ERROR;
import static org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.SENDING;
import static org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus.SENT;
/**
* Helper class for the local file sharing module.

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.local_file_transfer
package org.kiwix.kiwixmobile.localFileTransfer
import android.app.Activity
import android.content.BroadcastReceiver
@ -45,8 +45,8 @@ import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.FileTransferConfirmation
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.local_file_transfer.FileItem.FileStatus
import org.kiwix.kiwixmobile.local_file_transfer.KiwixWifiP2pBroadcastReceiver.P2pEventListener
import org.kiwix.kiwixmobile.localFileTransfer.FileItem.FileStatus
import org.kiwix.kiwixmobile.localFileTransfer.KiwixWifiP2pBroadcastReceiver.P2pEventListener
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
@ -93,15 +93,16 @@ class WifiDirectManager @Inject constructor(
// IP address of the file receiving device
private lateinit var fileReceiverDeviceAddress: InetAddress
private lateinit var filesForTransfer: ArrayList<FileItem>
private lateinit var filesForTransfer: List<FileItem>
// Whether the device is the file sender or not
var isFileSender = false
private set
private var hasSenderStartedConnection = false
/* Initialisations for using the WiFi P2P API */
fun startWifiDirectManager(filesForTransfer: ArrayList<FileItem>) {
fun startWifiDirectManager(filesForTransfer: List<FileItem>) {
this.filesForTransfer = filesForTransfer
isFileSender = filesForTransfer.isNotEmpty()
manager = activity.getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager
@ -355,7 +356,7 @@ class WifiDirectManager @Inject constructor(
fun onUserDeviceDetailsAvailable(userDevice: WifiP2pDevice?)
fun onConnectionToPeersLost()
fun updateListOfAvailablePeers(peers: WifiP2pDeviceList)
fun onFilesForTransferAvailable(filesForTransfer: ArrayList<FileItem>)
fun onFilesForTransferAvailable(filesForTransfer: List<FileItem>)
fun onFileStatusChanged(itemIndex: Int)
fun onFileTransferComplete()
}

View File

@ -16,7 +16,7 @@
*
*/
package org.kiwix.kiwixmobile.local_file_transfer.adapter
package org.kiwix.kiwixmobile.localFileTransfer.adapter
import android.net.wifi.p2p.WifiP2pDevice
import android.view.ViewGroup

View File

@ -16,7 +16,7 @@
*
*/
package org.kiwix.kiwixmobile.local_file_transfer.adapter
package org.kiwix.kiwixmobile.localFileTransfer.adapter
import android.net.wifi.p2p.WifiP2pDevice
import android.view.View

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.local_file_transfer.adapter
package org.kiwix.kiwixmobile.localFileTransfer.adapter
import android.net.wifi.p2p.WifiP2pDevice
import org.kiwix.kiwixmobile.core.base.adapter.BaseDelegateAdapter

View File

@ -1,406 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.local_file_transfer;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import android.net.Uri;
import android.net.wifi.p2p.WifiP2pDevice;
import android.net.wifi.p2p.WifiP2pDeviceList;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;
import org.kiwix.kiwixmobile.ActivityExtensionsKt;
import org.kiwix.kiwixmobile.R;
import org.kiwix.kiwixmobile.core.base.BaseActivity;
import org.kiwix.kiwixmobile.core.di.components.CoreComponent;
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower;
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog;
import org.kiwix.kiwixmobile.local_file_transfer.adapter.WifiP2pDelegate;
import org.kiwix.kiwixmobile.local_file_transfer.adapter.WifiPeerListAdapter;
/**
* Created by @Aditya-Sood as a part of GSoC 2019.
*
* This activity is the starting point for the module used for sharing zims between devices.
*
* The module is used for transferring ZIM files from one device to another, from within the
* app. Two devices are connected to each other using WiFi Direct, followed by file transfer.
*
* File transfer involves two phases:
* 1) Handshake with the selected peer device, using {@link PeerGroupHandshakeAsyncTask}
* 2) After handshake, starting the files transfer using {@link SenderDeviceAsyncTask} on the sender
* device and {@link ReceiverDeviceAsyncTask} files receiving device
*/
@SuppressLint("GoogleAppIndexingApiWarning")
public class LocalFileTransferActivity extends BaseActivity implements
WifiDirectManager.Callbacks {
// Not a typo, 'Log' tags have a length upper limit of 25 characters
public static final String TAG = "LocalFileTransferActvty";
public static final int REQUEST_ENABLE_LOCATION_SERVICES = 1;
private static final int PERMISSION_REQUEST_CODE_COARSE_LOCATION = 1;
private static final int PERMISSION_REQUEST_CODE_STORAGE_WRITE_ACCESS = 2;
@Inject AlertDialogShower alertDialogShower;
@Inject WifiDirectManager wifiDirectManager;
@Inject LocationManager locationManager;
@BindView(R.id.toolbar) Toolbar actionBar;
@BindView(R.id.text_view_device_name) TextView deviceName;
@BindView(R.id.progress_bar_searching_peers) ProgressBar searchingPeersProgressBar;
@BindView(R.id.list_peer_devices) RecyclerView peerDeviceList;
@BindView(R.id.text_view_empty_peer_list) TextView textViewPeerDevices;
@BindView(R.id.recycler_view_transfer_files) RecyclerView filesRecyclerView;
private boolean isFileSender = false; // Whether the device is the file sender or not
private ArrayList<FileItem> filesForTransfer = new ArrayList<>();
private FileListAdapter fileListAdapter;
private WifiPeerListAdapter wifiPeerListAdapter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_local_file_transfer);
/*
* Presence of file Uris decides whether the device with the activity open is a sender or receiver:
* - On the sender device, this activity is started from the app chooser post selection
* of files to share in the Library
* - On the receiver device, the activity is started directly from within the 'Get Content'
* activity, without any file Uris
* */
Intent filesIntent = getIntent();
ArrayList<Uri> fileUriArrayList;
fileUriArrayList = filesIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
isFileSender = (fileUriArrayList != null && fileUriArrayList.size() > 0);
setSupportActionBar(actionBar);
actionBar.setNavigationIcon(R.drawable.ic_close_white_24dp);
actionBar.setNavigationOnClickListener(v -> finish());
wifiPeerListAdapter = new WifiPeerListAdapter(
new WifiP2pDelegate(wifiP2pDevice -> {
wifiDirectManager.sendToDevice(wifiP2pDevice);
return Unit.INSTANCE;
}
)
);
peerDeviceList.setAdapter(wifiPeerListAdapter);
peerDeviceList.setLayoutManager(new LinearLayoutManager(this));
peerDeviceList.setHasFixedSize(true);
if (isFileSender) {
for (int i = 0; i < fileUriArrayList.size(); i++) {
filesForTransfer.add(new FileItem(fileUriArrayList.get(i)));
}
displayFileTransferProgress(filesForTransfer);
}
wifiDirectManager.startWifiDirectManager(filesForTransfer);
}
@Override
public boolean onCreateOptionsMenu(@NonNull Menu menu) {
getMenuInflater().inflate(R.menu.wifi_file_share_items, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.menu_item_search_devices) {
/* Permissions essential for this module */
if (!checkCoarseLocationAccessPermission()) {
return true;
}
if (!checkExternalStorageWritePermission()) {
return true;
}
/* Initiate discovery */
if (!wifiDirectManager.isWifiP2pEnabled()) {
requestEnableWifiP2pServices();
return true;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && !isLocationServiceEnabled()) {
requestEnableLocationServices();
return true;
}
showPeerDiscoveryProgressBar();
wifiDirectManager.discoverPeerDevices();
return true;
} else {
return super.onOptionsItemSelected(item);
}
}
private void showPeerDiscoveryProgressBar() { // Setup UI for searching peers
searchingPeersProgressBar.setVisibility(View.VISIBLE);
peerDeviceList.setVisibility(View.INVISIBLE);
textViewPeerDevices.setVisibility(View.INVISIBLE);
}
/* From WifiDirectManager.Callbacks interface */
@Override
public void onUserDeviceDetailsAvailable(@Nullable WifiP2pDevice userDevice) {
// Update UI with user device's details
if (userDevice != null) {
deviceName.setText(userDevice.deviceName);
Log.d(TAG, WifiDirectManager.getDeviceStatus(userDevice.status));
}
}
@Override
public void onConnectionToPeersLost() {
wifiPeerListAdapter.setItems(Collections.EMPTY_LIST);
}
@Override
public void onFilesForTransferAvailable(@NonNull ArrayList<FileItem> filesForTransfer) {
this.filesForTransfer = filesForTransfer;
displayFileTransferProgress(filesForTransfer);
}
private void displayFileTransferProgress(@NonNull ArrayList<FileItem> filesToSend) {
fileListAdapter = new FileListAdapter(filesToSend);
filesRecyclerView.setAdapter(fileListAdapter);
filesRecyclerView.setLayoutManager(new LinearLayoutManager(this));
}
@Override
public void onFileStatusChanged(int itemIndex) {
fileListAdapter.notifyItemChanged(itemIndex);
}
@Override
public void updateListOfAvailablePeers(@NonNull WifiP2pDeviceList peers) {
final List<WifiP2pDevice> deviceList = new ArrayList(peers.getDeviceList());
searchingPeersProgressBar.setVisibility(View.GONE);
peerDeviceList.setVisibility(View.VISIBLE);
wifiPeerListAdapter.setItems(deviceList);
if (deviceList.size() == 0) {
Log.d(LocalFileTransferActivity.TAG, "No devices found");
}
}
@Override
public void onFileTransferComplete() {
finish();
}
/* Helper methods used for checking permissions and states of services */
private boolean checkCoarseLocationAccessPermission() { // Required by Android to detect wifi-p2p peers
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION)
== PackageManager.PERMISSION_DENIED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.ACCESS_COARSE_LOCATION)) {
alertDialogShower.show(KiwixDialog.LocationPermissionRationale.INSTANCE,
(Function0<Unit>) () -> {
ActivityCompat.requestPermissions(this,
new String[] { Manifest.permission.ACCESS_COARSE_LOCATION },
PERMISSION_REQUEST_CODE_COARSE_LOCATION);
return Unit.INSTANCE;
});
} else {
ActivityCompat.requestPermissions(this,
new String[] { Manifest.permission.ACCESS_COARSE_LOCATION },
PERMISSION_REQUEST_CODE_COARSE_LOCATION);
}
return false;
} else {
return true; // Control reaches here: Either permission granted at install time, or at the time of request
}
}
private boolean checkExternalStorageWritePermission() { // To access and store the zims
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_DENIED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
alertDialogShower.show(KiwixDialog.StoragePermissionRationale.INSTANCE,
new Function0<Unit>() {
@Override public Unit invoke() {
ActivityCompat.requestPermissions(LocalFileTransferActivity.this,
new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE },
PERMISSION_REQUEST_CODE_STORAGE_WRITE_ACCESS);
return Unit.INSTANCE;
}
});
} else {
ActivityCompat.requestPermissions(this,
new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE },
PERMISSION_REQUEST_CODE_STORAGE_WRITE_ACCESS);
}
return false;
} else {
return true; // Control reaches here: Either permission granted at install time, or at the time of request
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
if (grantResults[0] == PackageManager.PERMISSION_DENIED) {
switch (requestCode) {
case PERMISSION_REQUEST_CODE_COARSE_LOCATION: {
Log.e(TAG, "Location permission not granted");
showToast(this, R.string.permission_refused_location, Toast.LENGTH_LONG);
finish();
break;
}
case PERMISSION_REQUEST_CODE_STORAGE_WRITE_ACCESS: {
Log.e(TAG, "Storage write permission not granted");
showToast(this, R.string.permission_refused_storage, Toast.LENGTH_LONG);
finish();
break;
}
default: {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
break;
}
}
}
}
private boolean isLocationServiceEnabled() {
return isProviderEnabled(LocationManager.GPS_PROVIDER)
|| isProviderEnabled(LocationManager.NETWORK_PROVIDER);
}
private boolean isProviderEnabled(String locationProvider) {
try {
return locationManager.isProviderEnabled(locationProvider);
} catch (SecurityException | IllegalArgumentException ex) {
ex.printStackTrace();
return false;
}
}
private void requestEnableLocationServices() {
alertDialogShower.show(KiwixDialog.EnableLocationServices.INSTANCE,
new Function0<Unit>() {
@Override public Unit invoke() {
startActivityForResult(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS),
REQUEST_ENABLE_LOCATION_SERVICES);
return Unit.INSTANCE;
}
},
new Function0<Unit>() {
@Override public Unit invoke() {
showToast(LocalFileTransferActivity.this, R.string.discovery_needs_location,
Toast.LENGTH_SHORT);
return Unit.INSTANCE;
}
});
}
private void requestEnableWifiP2pServices() {
alertDialogShower.show(KiwixDialog.EnableWifiP2pServices.INSTANCE,
new Function0<Unit>() {
@Override public Unit invoke() {
startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS));
return Unit.INSTANCE;
}
},
new Function0<Unit>() {
@Override public Unit invoke() {
showToast(LocalFileTransferActivity.this, R.string.discovery_needs_wifi,
Toast.LENGTH_SHORT);
return Unit.INSTANCE;
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
switch (requestCode) {
case REQUEST_ENABLE_LOCATION_SERVICES: {
if (!isLocationServiceEnabled()) {
showToast(this, R.string.permission_refused_location, Toast.LENGTH_LONG);
}
break;
}
default: {
super.onActivityResult(requestCode, resultCode, data);
break;
}
}
}
/* Miscellaneous helper methods */
static void showToast(Context context, int stringResource, int duration) {
showToast(context, context.getString(stringResource), duration);
}
static void showToast(Context context, String text, int duration) {
Toast.makeText(context, text, duration).show();
}
@Override protected void onDestroy() {
wifiDirectManager.stopWifiDirectManager();
super.onDestroy();
}
@Override protected void injection(CoreComponent coreComponent) {
ActivityExtensionsKt.getKiwixActivityComponent(this).inject(this);
}
}

View File

@ -54,7 +54,7 @@ import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BookOnDiskDelegate
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskAdapter
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem
import org.kiwix.kiwixmobile.local_file_transfer.LocalFileTransferActivity
import org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferActivity
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel.FileSelectActions.RequestMultiSelection

View File

@ -18,7 +18,10 @@
package org.kiwix.kiwixmobile.nav.destination.library
import android.content.Intent
import android.net.ConnectivityManager
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
@ -28,23 +31,170 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import eu.mhutti1.utils.storage.StorageDevice
import eu.mhutti1.utils.storage.StorageSelectDialog
import kotlinx.android.synthetic.main.fragment_destination_download.libraryErrorText
import kotlinx.android.synthetic.main.fragment_destination_download.libraryList
import kotlinx.android.synthetic.main.fragment_destination_download.librarySwipeRefresh
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.cachedComponent
import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.base.BaseFragment
import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.navigate
import org.kiwix.kiwixmobile.core.downloader.Downloader
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.viewModel
import org.kiwix.kiwixmobile.core.extensions.snack
import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.utils.BookUtils
import org.kiwix.kiwixmobile.core.utils.NetworkUtils
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.SimpleTextListener
import org.kiwix.kiwixmobile.zim_manager.library_view.LibraryFragment
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog
import org.kiwix.kiwixmobile.zim_manager.NetworkState
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel
import org.kiwix.kiwixmobile.zim_manager.library_view.AvailableSpaceCalculator
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryAdapter
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryDelegate
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem
import javax.inject.Inject
class OnlineLibraryFragment : LibraryFragment(), FragmentActivityExtensions {
class OnlineLibraryFragment : BaseFragment(), FragmentActivityExtensions {
@Inject lateinit var conMan: ConnectivityManager
@Inject lateinit var downloader: Downloader
@Inject lateinit var dialogShower: DialogShower
@Inject lateinit var sharedPreferenceUtil: SharedPreferenceUtil
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
@Inject lateinit var bookUtils: BookUtils
@Inject lateinit var availableSpaceCalculator: AvailableSpaceCalculator
private val zimManageViewModel by lazy {
requireActivity().viewModel<ZimManageViewModel>(viewModelFactory)
}
private val libraryAdapter: LibraryAdapter by lazy {
LibraryAdapter(
LibraryDelegate.BookDelegate(bookUtils, ::onBookItemClick),
LibraryDelegate.DownloadDelegate {
dialogShower.show(
KiwixDialog.YesNoDialog.StopDownload,
{ downloader.cancelDownload(it.downloadId) })
},
LibraryDelegate.DividerDelegate
)
}
private val noWifiWithWifiOnlyPreferenceSet
get() = sharedPreferenceUtil.prefWifiOnly && !NetworkUtils.isWiFi(requireContext())
private val isNotConnected get() = conMan.activeNetworkInfo?.isConnected == false
private fun onRefreshStateChange(isRefreshing: Boolean?) {
librarySwipeRefresh.isRefreshing = isRefreshing!!
}
private fun onNetworkStateChange(networkState: NetworkState?) {
when (networkState) {
NetworkState.CONNECTED -> {
}
NetworkState.NOT_CONNECTED -> {
if (libraryAdapter.itemCount > 0) {
noInternetSnackbar()
} else {
libraryErrorText.setText(R.string.no_network_connection)
libraryErrorText.visibility = View.VISIBLE
}
librarySwipeRefresh.isRefreshing = false
}
}
}
private fun noInternetSnackbar() {
view?.snack(
R.string.no_network_connection,
R.string.menu_settings,
::openNetworkSettings
)
}
private fun openNetworkSettings() {
startActivity(Intent(Settings.ACTION_WIFI_SETTINGS))
}
private fun onLibraryItemsChange(it: List<LibraryListItem>?) {
libraryAdapter.items = it!!
if (it.isEmpty()) {
libraryErrorText.setText(
if (isNotConnected) R.string.no_network_connection
else R.string.no_items_msg
)
libraryErrorText.visibility = View.VISIBLE
} else {
libraryErrorText.visibility = View.GONE
}
}
private fun refreshFragment() {
if (isNotConnected) {
noInternetSnackbar()
} else {
zimManageViewModel.requestDownloadLibrary.onNext(Unit)
}
}
private fun downloadFile(book: LibraryNetworkEntity.Book) {
downloader.download(book)
}
private fun storeDeviceInPreferences(storageDevice: StorageDevice) {
sharedPreferenceUtil.putPrefStorage(storageDevice.name)
}
private fun onBookItemClick(item: LibraryListItem.BookItem) {
when {
isNotConnected -> {
noInternetSnackbar()
return
}
noWifiWithWifiOnlyPreferenceSet -> {
dialogShower.show(KiwixDialog.YesNoDialog.WifiOnly, {
sharedPreferenceUtil.putPrefWifiOnly(false)
downloadFile(item.book)
})
return
}
else -> availableSpaceCalculator.hasAvailableSpaceFor(item,
{ downloadFile(item.book) },
{
libraryList.snack(
getString(R.string.download_no_space) +
"\n" + getString(R.string.space_available) + " " +
it,
R.string.download_change_storage,
::showStorageSelectDialog
)
})
}
}
private fun showStorageSelectDialog() = StorageSelectDialog()
.apply {
onSelectAction = ::storeDeviceInPreferences
}
.show(requireFragmentManager(), getString(R.string.pref_storage))
override fun inject(baseActivity: BaseActivity) {
baseActivity.cachedComponent.inject(this)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
super<LibraryFragment>.onCreateOptionsMenu(menu, inflater)
super<BaseFragment>.onCreateOptionsMenu(menu, inflater)
inflater.inflate(R.menu.menu_zim_manager, menu)
val searchItem = menu.findItem(R.id.action_search)
val getZimItem = menu.findItem(R.id.get_zim_nearby_device)
@ -68,6 +218,11 @@ class OnlineLibraryFragment : LibraryFragment(), FragmentActivityExtensions {
return super.onOptionsItemSelected(item)
}
override fun onDestroyView() {
super.onDestroyView()
libraryList.adapter = null
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -85,4 +240,19 @@ class OnlineLibraryFragment : LibraryFragment(), FragmentActivityExtensions {
activity.setupDrawerToggle(toolbar)
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
librarySwipeRefresh.setOnRefreshListener(::refreshFragment)
libraryList.run {
adapter = libraryAdapter
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
setHasFixedSize(true)
}
zimManageViewModel.libraryItems.observe(viewLifecycleOwner, Observer(::onLibraryItemsChange))
zimManageViewModel.libraryListIsRefreshing.observe(
viewLifecycleOwner, Observer(::onRefreshStateChange)
)
zimManageViewModel.networkStates.observe(viewLifecycleOwner, Observer(::onNetworkStateChange))
}
}

View File

@ -19,8 +19,9 @@
package org.kiwix.kiwixmobile.settings
import android.os.Bundle
import android.os.Environment
import androidx.core.content.ContextCompat
import androidx.preference.Preference
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.settings.CorePrefsFragment
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil.PREF_STORAGE
@ -33,13 +34,13 @@ class KiwixPrefsFragment : CorePrefsFragment() {
}
override fun setStorage() {
if (Environment.isExternalStorageEmulated()) {
findPreference<Preference>(PREF_STORAGE)?.title =
sharedPreferenceUtil.getPrefStorageTitle("Internal")
} else {
findPreference<Preference>(PREF_STORAGE)?.title =
sharedPreferenceUtil.getPrefStorageTitle("External")
}
findPreference<Preference>(PREF_STORAGE)?.title = getString(
if (sharedPreferenceUtil.prefStorage == internalStorage()) R.string.internal_storage
else R.string.external_storage
)
findPreference<Preference>(PREF_STORAGE)?.summary = storageCalculator.calculateAvailableSpace()
}
private fun internalStorage(): String? =
ContextCompat.getExternalFilesDirs(requireContext(), null).firstOrNull()?.path
}

View File

@ -21,27 +21,45 @@ package org.kiwix.kiwixmobile.zim_manager
import android.util.Log
import org.kiwix.kiwixmobile.zim_manager.FileSystemCapability.CANNOT_WRITE_4GB
import org.kiwix.kiwixmobile.zim_manager.FileSystemCapability.CAN_WRITE_4GB
import org.kiwix.kiwixmobile.zim_manager.FileSystemCapability.INCONCLUSIVE
import java.io.File
import java.io.RandomAccessFile
class FileWritingFileSystemChecker : FileSystemChecker {
override fun checkFilesystemSupports4GbFiles(path: String): FileSystemCapability {
with(File("$path/large_file_test.txt")) {
val resultFile = File("$path/.file_writing_result")
if (resultFile.exists()) {
when (val capability = readCapability(resultFile)) {
CAN_WRITE_4GB,
CANNOT_WRITE_4GB -> return capability
}
}
return with(File("$path/large_file_test.txt"), {
deleteIfExists()
try {
RandomAccessFile(this.path, "rw").use {
it.setLength(Fat32Checker.FOUR_GIGABYTES_IN_BYTES)
return@checkFilesystemSupports4GbFiles CAN_WRITE_4GB
CAN_WRITE_4GB.alsoSaveTo(resultFile)
}
} catch (e: Exception) {
e.printStackTrace()
Log.d("Fat32Checker", e.message)
return@checkFilesystemSupports4GbFiles CANNOT_WRITE_4GB
CANNOT_WRITE_4GB.alsoSaveTo(resultFile)
} finally {
deleteIfExists()
}
}
})
}
private fun readCapability(resultFile: File) =
try {
FileSystemCapability.valueOf(resultFile.readText())
} catch (illegalArgumentException: IllegalArgumentException) {
INCONCLUSIVE
}
private fun FileSystemCapability.alsoSaveTo(resultFile: File) =
also { resultFile.writeText(name) }
}
private fun File.deleteIfExists() {

View File

@ -1,220 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.zim_manager.library_view
import android.content.Intent
import android.net.ConnectivityManager
import android.os.Bundle
import android.provider.Settings
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import eu.mhutti1.utils.storage.StorageDevice
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
import org.kiwix.kiwixmobile.R
import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.base.BaseFragment
import org.kiwix.kiwixmobile.core.downloader.Downloader
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity.Book
import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.viewModel
import org.kiwix.kiwixmobile.core.extensions.snack
import org.kiwix.kiwixmobile.core.utils.BookUtils
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.YesNoDialog.StopDownload
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.YesNoDialog.WifiOnly
import org.kiwix.kiwixmobile.core.utils.NetworkUtils
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.kiwixActivityComponent
import org.kiwix.kiwixmobile.zim_manager.NetworkState
import org.kiwix.kiwixmobile.zim_manager.NetworkState.CONNECTED
import org.kiwix.kiwixmobile.zim_manager.NetworkState.NOT_CONNECTED
import org.kiwix.kiwixmobile.zim_manager.ZimManageViewModel
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryAdapter
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryDelegate.BookDelegate
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryDelegate.DividerDelegate
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryDelegate.DownloadDelegate
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem
import org.kiwix.kiwixmobile.zim_manager.library_view.adapter.LibraryListItem.BookItem
import javax.inject.Inject
open class LibraryFragment : BaseFragment() {
@Inject lateinit var conMan: ConnectivityManager
@Inject lateinit var downloader: Downloader
@Inject lateinit var dialogShower: DialogShower
@Inject lateinit var sharedPreferenceUtil: SharedPreferenceUtil
@Inject lateinit var viewModelFactory: ViewModelProvider.Factory
@Inject lateinit var bookUtils: BookUtils
@Inject lateinit var availableSpaceCalculator: AvailableSpaceCalculator
protected val zimManageViewModel by lazy {
requireActivity().viewModel<ZimManageViewModel>(viewModelFactory)
}
private val libraryAdapter: LibraryAdapter by lazy {
LibraryAdapter(
BookDelegate(bookUtils, ::onBookItemClick),
DownloadDelegate {
dialogShower.show(StopDownload, { downloader.cancelDownload(it.downloadId) })
},
DividerDelegate
)
}
private val noWifiWithWifiOnlyPreferenceSet
get() = sharedPreferenceUtil.prefWifiOnly && !NetworkUtils.isWiFi(requireContext())
private val isNotConnected get() = conMan.activeNetworkInfo?.isConnected == false
override fun inject(baseActivity: BaseActivity) {
baseActivity.kiwixActivityComponent.inject(this)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = inflater.inflate(R.layout.activity_library, container, false)
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
librarySwipeRefresh.setOnRefreshListener(::refreshFragment)
libraryList.run {
adapter = libraryAdapter
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
setHasFixedSize(true)
}
zimManageViewModel.libraryItems.observe(viewLifecycleOwner, Observer(::onLibraryItemsChange))
zimManageViewModel.libraryListIsRefreshing.observe(
viewLifecycleOwner, Observer(::onRefreshStateChange)
)
zimManageViewModel.networkStates.observe(viewLifecycleOwner, Observer(::onNetworkStateChange))
}
private fun onRefreshStateChange(isRefreshing: Boolean?) {
librarySwipeRefresh.isRefreshing = isRefreshing!!
}
private fun onNetworkStateChange(networkState: NetworkState?) {
when (networkState) {
CONNECTED -> {
}
NOT_CONNECTED -> {
if (libraryAdapter.itemCount > 0) {
noInternetSnackbar()
} else {
libraryErrorText.setText(R.string.no_network_connection)
libraryErrorText.visibility = VISIBLE
}
librarySwipeRefresh.isRefreshing = false
}
}
}
private fun noInternetSnackbar() {
view?.snack(
R.string.no_network_connection,
R.string.menu_settings,
::openNetworkSettings
)
}
private fun openNetworkSettings() {
startActivity(Intent(Settings.ACTION_WIFI_SETTINGS))
}
private fun onLibraryItemsChange(it: List<LibraryListItem>?) {
libraryAdapter.items = it!!
if (it.isEmpty()) {
libraryErrorText.setText(
if (isNotConnected) R.string.no_network_connection
else R.string.no_items_msg
)
libraryErrorText.visibility = VISIBLE
} else {
libraryErrorText.visibility = GONE
}
}
private fun refreshFragment() {
if (isNotConnected) {
noInternetSnackbar()
} else {
zimManageViewModel.requestDownloadLibrary.onNext(Unit)
}
}
private fun downloadFile(book: Book) {
downloader.download(book)
}
private fun storeDeviceInPreferences(storageDevice: StorageDevice) {
sharedPreferenceUtil.putPrefStorage(storageDevice.name)
sharedPreferenceUtil.putPrefStorageTitle(
getString(
if (storageDevice.isInternal) R.string.internal_storage
else R.string.external_storage
)
)
}
private fun onBookItemClick(item: BookItem) {
when {
isNotConnected -> {
noInternetSnackbar()
return
}
noWifiWithWifiOnlyPreferenceSet -> {
dialogShower.show(WifiOnly, {
sharedPreferenceUtil.putPrefWifiOnly(false)
downloadFile(item.book)
})
return
}
else -> availableSpaceCalculator.hasAvailableSpaceFor(item,
{ downloadFile(item.book) },
{
libraryList.snack(
getString(R.string.download_no_space) +
"\n" + getString(R.string.space_available) + " " +
it,
R.string.download_change_storage,
::showStorageSelectDialog
)
})
}
}
private fun showStorageSelectDialog() = StorageSelectDialog()
.apply {
onSelectAction = ::storeDeviceInPreferences
}
.show(requireFragmentManager(), getString(R.string.pref_storage))
}

View File

@ -1,34 +0,0 @@
<?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"
android:animateLayoutChanges="true"
tools:context="org.kiwix.kiwixmobile.zim_manager.library_view.LibraryFragment">
<TextView
android:id="@+id/libraryErrorText"
style="@style/no_list_content_text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/librarySwipeRefresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/libraryList"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -6,7 +6,7 @@
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:fitsSystemWindows="true"
tools:context="org.kiwix.kiwixmobile.local_file_transfer.LocalFileTransferActivity">
tools:context="org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferActivity">
<include layout="@layout/layout_standard_app_bar" />

View File

@ -40,7 +40,7 @@
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginBottom="8dp"
android:background="@color/startServerGreen"
android:backgroundTint="@color/startServerGreen"
android:text="@string/start_server_label"
android:textColor="@color/white"
app:layout_constraintBottom_toBottomOf="parent"

View File

@ -2,7 +2,7 @@
<menu 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"
tools:context="org.kiwix.kiwixmobile.local_file_transfer.LocalFileTransferActivity">
tools:context="org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferActivity">
<item
android:id="@+id/menu_item_search_devices"

View File

@ -83,7 +83,7 @@ object Versions {
const val core_ktx: String = "1.3.1"
const val kiwixlib: String = "9.3.1"
const val kiwixlib: String = "9.4.0"
const val material: String = "1.2.0"

View File

@ -19,16 +19,25 @@
android:fullBackupContent="@xml/backup_rules"
android:hardwareAccelerated="true"
android:label="@string/app_name"
android:resizeableActivity="true"
android:supportsRtl="true"
android:theme="@style/KiwixTheme"
android:usesCleartextTraffic="true"
tools:targetApi="m">
tools:targetApi="n">
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
<meta-data
android:name="com.samsung.android.keepalive.density"
android:value="true" />
<!-- Version >= 3.0. DeX Dual Mode support -->
<meta-data
android:name="com.samsung.android.multidisplay.keep_process_alive"
android:value="true" />
<activity
android:name=".search.SearchActivity"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".error.ErrorActivity"
android:process=":error_activity" />

View File

@ -20,6 +20,7 @@ package org.kiwix.kiwixmobile.core.error
import android.os.Bundle
import android.view.View
import kotlinx.android.synthetic.main.activity_kiwix_error.allowCrash
import kotlinx.android.synthetic.main.activity_kiwix_error.messageText
import kotlinx.android.synthetic.main.activity_kiwix_error.textView2
import org.kiwix.kiwixmobile.core.R
@ -30,16 +31,16 @@ class DiagnosticReportActivity : ErrorActivity() {
super.onCreate(savedInstanceState)
textView2.setText(R.string.diagnostic_report)
messageText.setText(R.string.diagnostic_report_message)
allowCrashCheckbox.visibility = View.GONE
allowCrash.visibility = View.GONE
}
override fun restartApp() {
finish()
}
override fun getSubject() = "Somebody has sent a Diagnostic Report "
override val subject = "Somebody has sent a Diagnostic Report "
override fun getBody() = """
override val initialBody = """
Hi Kiwix Developers,
I am having an issue with the app and would like you to check these details

View File

@ -1,250 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.error;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.widget.Button;
import android.widget.CheckBox;
import androidx.core.content.ContextCompat;
import butterknife.BindView;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.List;
import javax.inject.Inject;
import org.jetbrains.annotations.NotNull;
import org.kiwix.kiwixmobile.core.R;
import org.kiwix.kiwixmobile.core.R2;
import org.kiwix.kiwixmobile.core.base.BaseActivity;
import org.kiwix.kiwixmobile.core.dao.NewBookDao;
import org.kiwix.kiwixmobile.core.di.components.CoreComponent;
import org.kiwix.kiwixmobile.core.entity.LibraryNetworkEntity;
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer;
import org.kiwix.kiwixmobile.core.utils.files.FileLogger;
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.adapter.BooksOnDiskListItem.BookOnDisk;
import org.kiwix.kiwixmobile.zim_manager.MountInfo;
import org.kiwix.kiwixmobile.zim_manager.MountPointProducer;
import static androidx.core.content.FileProvider.getUriForFile;
import static org.kiwix.kiwixmobile.core.utils.LanguageUtils.getCurrentLocale;
public class ErrorActivity extends BaseActivity {
public static final String EXCEPTION_KEY = "exception";
@Inject
NewBookDao bookDao;
@Inject
ZimReaderContainer zimReaderContainer;
@Inject
MountPointProducer mountPointProducer;
@Inject
FileLogger fileLogger;
@BindView(R2.id.reportButton)
Button reportButton;
@BindView(R2.id.restartButton)
Button restartButton;
@BindView(R2.id.allowLanguage)
CheckBox allowLanguageCheckbox;
@BindView(R2.id.allowZims)
CheckBox allowZimsCheckbox;
@BindView(R2.id.allowCrash)
CheckBox allowCrashCheckbox;
@BindView(R2.id.allowLogs)
CheckBox allowLogsCheckbox;
@BindView(R2.id.allowDeviceDetails)
CheckBox allowDeviceDetailsCheckbox;
@BindView(R2.id.allowFileSystemDetails)
CheckBox allowFileSystemDetailsCheckbox;
private static void killCurrentProcess() {
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(10);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_kiwix_error);
Intent callingIntent = getIntent();
Bundle extras = callingIntent.getExtras();
final Throwable exception;
if (extras != null && safeContains(extras, EXCEPTION_KEY)) {
exception = (Throwable) extras.getSerializable(EXCEPTION_KEY);
} else {
exception = null;
}
reportButton.setOnClickListener(v -> {
Intent emailIntent = new Intent(Intent.ACTION_SEND);
emailIntent.setType("vnd.android.cursor.dir/email");
emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[] { "android-crash-feedback@kiwix.org" });
emailIntent.putExtra(Intent.EXTRA_SUBJECT, getSubject());
String body = getBody();
if (allowLogsCheckbox.isChecked()) {
File file = fileLogger.writeLogFile(this);
Uri path = getUriForFile(this, getApplicationContext().getPackageName()+ ".fileprovider", file);
emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
emailIntent.putExtra(Intent.EXTRA_STREAM, path);
}
if (allowCrashCheckbox.isChecked() && exception != null) {
body += "Exception Details:\n\n" +
toStackTraceString(exception) +
"\n\n";
}
if (allowZimsCheckbox.isChecked()) {
List<BookOnDisk> books = bookDao.getBooks();
StringBuilder sb = new StringBuilder();
for (BookOnDisk bookOnDisk : books) {
final LibraryNetworkEntity.Book book = bookOnDisk.getBook();
String bookString = book.getTitle() +
":\nArticles: [" + book.getArticleCount() +
"]\nCreator: [" + book.getCreator() +
"]\n";
sb.append(bookString);
}
String allZimFiles = sb.toString();
String currentZimFile = zimReaderContainer.getZimCanonicalPath();
body += "Curent Zim File:\n" +
currentZimFile +
"\n\nAll Zim Files in DB:\n" +
allZimFiles +
"\n\n";
}
if (allowLanguageCheckbox.isChecked()) {
body += "Current Locale:\n" +
getCurrentLocale(getApplicationContext()) +
"\n\n";
}
if (allowDeviceDetailsCheckbox.isChecked()) {
body += "Device Details:\n" +
"Device:[" + Build.DEVICE
+ "]\nModel:[" + Build.MODEL
+ "]\nManufacturer:[" + Build.MANUFACTURER
+ "]\nTime:[" + Build.TIME
+ "]\nAndroid Version:[" + Build.VERSION.RELEASE
+ "]\nApp Version:[" + getVersionName() + " " + getVersionCode()
+ "]" +
"\n\n";
}
if (allowFileSystemDetailsCheckbox.isChecked()) {
body += "Mount Points\n";
for (MountInfo mountInfo : mountPointProducer.produce()) {
body += mountInfo + "\n";
}
body += "\nExternal Directories\n";
for (File externalFilesDir : ContextCompat.getExternalFilesDirs(this, null)) {
body += (externalFilesDir != null ? externalFilesDir.getPath() : "null") + "\n";
}
}
emailIntent.putExtra(Intent.EXTRA_TEXT, body);
startActivityForResult(Intent.createChooser(emailIntent, "Send email..."), 1);
});
restartButton.setOnClickListener(v -> onRestartClicked());
}
private boolean safeContains(Bundle extras, String key) {
try {
return extras.containsKey(key);
} catch (RuntimeException ignore) {
return false;
}
}
private void onRestartClicked() {
restartApp();
}
@NotNull protected String getSubject() {
return "Someone has reported a crash";
}
@NotNull protected String getBody() {
return "Hi Kiwix Developers!\n" +
"The Android app crashed, here are some details to help fix it:\n\n";
}
private int getVersionCode() {
try {
return getPackageManager()
.getPackageInfo(getPackageName(), 0).versionCode;
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
}
private String getVersionName() {
try {
return getPackageManager()
.getPackageInfo(getPackageName(), 0).versionName;
} catch (PackageManager.NameNotFoundException e) {
throw new RuntimeException(e);
}
}
private String toStackTraceString(Throwable exception) {
StringWriter stringWriter = new StringWriter();
exception.printStackTrace(new PrintWriter(stringWriter));
return stringWriter.toString();
}
void restartApp() {
startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName()));
finish();
killCurrentProcess();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
restartApp();
}
@Override protected void injection(CoreComponent coreComponent) {
coreComponent.inject(this);
}
}

View File

@ -0,0 +1,225 @@
/*
* 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.error
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Process
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import kotlinx.android.synthetic.main.activity_kiwix_error.allowCrash
import kotlinx.android.synthetic.main.activity_kiwix_error.allowDeviceDetails
import kotlinx.android.synthetic.main.activity_kiwix_error.allowFileSystemDetails
import kotlinx.android.synthetic.main.activity_kiwix_error.allowLanguage
import kotlinx.android.synthetic.main.activity_kiwix_error.allowLogs
import kotlinx.android.synthetic.main.activity_kiwix_error.allowZims
import kotlinx.android.synthetic.main.activity_kiwix_error.reportButton
import kotlinx.android.synthetic.main.activity_kiwix_error.restartButton
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.base.BaseActivity
import org.kiwix.kiwixmobile.core.dao.NewBookDao
import org.kiwix.kiwixmobile.core.di.components.CoreComponent
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.utils.LanguageUtils.Companion.getCurrentLocale
import org.kiwix.kiwixmobile.core.utils.files.FileLogger
import org.kiwix.kiwixmobile.zim_manager.MountPointProducer
import java.io.PrintWriter
import java.io.StringWriter
import javax.inject.Inject
import kotlin.system.exitProcess
private const val STATUS = 10
private const val ZERO = 0
open class ErrorActivity : BaseActivity() {
@Inject
lateinit var bookDao: NewBookDao
@Inject
lateinit var zimReaderContainer: ZimReaderContainer
@Inject
lateinit var mountPointProducer: MountPointProducer
@Inject
lateinit var fileLogger: FileLogger
private var exception: Throwable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_kiwix_error)
val extras = intent.extras
exception = if (extras != null && safeContains(extras)) {
extras.getSerializable(EXCEPTION_KEY) as Throwable
} else {
null
}
setupReportButton()
restartButton.setOnClickListener { restartApp() }
}
private fun setupReportButton() {
reportButton.setOnClickListener {
startActivityForResult(
Intent.createChooser(emailIntent(), "Send email..."), 1
)
}
}
private fun emailIntent(): Intent {
return Intent(Intent.ACTION_SEND).apply {
type = "vnd.android.cursor.dir/email"
putExtra(
Intent.EXTRA_EMAIL,
arrayOf("android-crash-feedback@kiwix.org")
)
putExtra(Intent.EXTRA_SUBJECT, subject)
putExtra(Intent.EXTRA_TEXT, buildBody())
if (allowLogs.isChecked) {
val file = fileLogger.writeLogFile(this@ErrorActivity)
val path =
FileProvider.getUriForFile(
this@ErrorActivity,
applicationContext.packageName + ".fileprovider",
file
)
addFlags(android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra(android.content.Intent.EXTRA_STREAM, path)
}
}
}
private fun buildBody(): String = """
$initialBody
${if (allowCrash.isChecked && exception != null) exceptionDetails() else ""}
${if (allowZims.isChecked) zimFiles() else ""}
${if (allowLanguage.isChecked) languageLocale() else ""}
${if (allowDeviceDetails.isChecked) deviceDetails() else ""}
${if (allowFileSystemDetails.isChecked) systemDetails() else ""}
""".trimIndent()
private fun exceptionDetails(): String =
"""
Exception Details:
${toStackTraceString(exception!!)}
""".trimIndent()
private fun zimFiles(): String {
val allZimFiles = bookDao.getBooks().joinToString {
"""
${it.book.getTitle()}:
Articles: [${it.book.getArticleCount()}]
Creator: [${it.book.getCreator()}]
""".trimIndent()
}
return """
Current Zim File:
${zimReaderContainer.zimCanonicalPath}
All Zim Files in DB:
$allZimFiles
""".trimIndent()
}
private fun languageLocale(): String = """
Current Locale:
${getCurrentLocale(applicationContext)}
""".trimIndent()
private fun deviceDetails(): String = """
BluetoothClass.Device Details:
Device:[${Build.DEVICE}]
Model:[${Build.MODEL}]
Manufacturer:[${Build.MANUFACTURER}]
Time:[${Build.TIME}]
Android Version:[${Build.VERSION.RELEASE}]
App Version:[$versionName $versionCode]
""".trimIndent()
private fun systemDetails(): String = """
Mount Points
${mountPointProducer.produce().joinToString { "$it\n" }}
External Directories
${externalFileDetails()}
""".trimIndent()
private fun externalFileDetails(): String =
ContextCompat.getExternalFilesDirs(this, null).joinToString("\n") { it?.path ?: "null" }
private fun safeContains(extras: Bundle): Boolean {
return try {
extras.containsKey(EXCEPTION_KEY)
} catch (ignore: RuntimeException) {
false
}
}
protected open val subject: String
get() = "Someone has reported a crash"
protected open val initialBody: String
get() = """
Hi Kiwix Developers!
The Android app crashed, here are some details to help fix it:
""".trimIndent()
private val versionCode: Int
get() = packageManager
.getPackageInfo(packageName, ZERO).versionCode
private val versionName: String
get() = packageManager
.getPackageInfo(packageName, ZERO).versionName
private fun toStackTraceString(exception: Throwable): String =
StringWriter().apply {
exception.printStackTrace(PrintWriter(this))
}.toString()
open fun restartApp() {
startActivity(packageManager.getLaunchIntentForPackage(packageName))
finish()
killCurrentProcess()
}
public override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
super.onActivityResult(requestCode, resultCode, data)
restartApp()
}
override fun injection(coreComponent: CoreComponent) {
coreComponent.inject(this)
}
companion object {
const val EXCEPTION_KEY = "exception"
private fun killCurrentProcess() {
Process.killProcess(Process.myPid())
exitProcess(STATUS)
}
}
}

View File

@ -1,90 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.help;
import android.animation.ObjectAnimator;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import java.util.Map;
import org.kiwix.kiwixmobile.core.R;
import org.kiwix.kiwixmobile.core.R2;
import static org.kiwix.kiwixmobile.core.utils.AnimationUtils.collapse;
import static org.kiwix.kiwixmobile.core.utils.AnimationUtils.expand;
class HelpAdapter extends RecyclerView.Adapter<HelpAdapter.Item> {
private final String[] titles;
private final String[] descriptions;
HelpAdapter(Map<String, String> titleDescriptionMap) {
this.titles = titleDescriptionMap.keySet().toArray(new String[0]);
this.descriptions = titleDescriptionMap.values().toArray(new String[0]);
}
@NonNull
@Override
public Item onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new Item(LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_help, parent, false));
}
@Override
public void onBindViewHolder(@NonNull Item holder, int position) {
holder.title.setText(titles[position]);
holder.description.setText(descriptions[position]);
}
@Override
public int getItemCount() {
return titles.length;
}
class Item extends RecyclerView.ViewHolder {
@BindView(R2.id.item_help_title)
TextView title;
@BindView(R2.id.item_help_description)
TextView description;
@BindView(R2.id.item_help_toggle_expand)
ImageView toggleDescriptionVisibility;
Item(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
}
@OnClick({ R2.id.item_help_title, R2.id.item_help_toggle_expand })
void toggleDescriptionVisibility() {
if (description.getVisibility() == View.GONE) {
ObjectAnimator.ofFloat(toggleDescriptionVisibility, "rotation", 0, 180).start();
expand(description);
} else {
ObjectAnimator.ofFloat(toggleDescriptionVisibility, "rotation", 180, 360).start();
collapse(description);
}
}
}
}

View File

@ -0,0 +1,73 @@
/*
* 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.help
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.item_help.item_help_description
import kotlinx.android.synthetic.main.item_help.item_help_title
import kotlinx.android.synthetic.main.item_help.item_help_toggle_expand
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.utils.AnimationUtils.collapse
import org.kiwix.kiwixmobile.core.utils.AnimationUtils.expand
import org.kiwix.kiwixmobile.core.base.adapter.BaseViewHolder
import org.kiwix.kiwixmobile.core.extensions.ViewGroupExtensions.inflate
internal class HelpAdapter(titleDescriptionMap: Map<String, String>) :
RecyclerView.Adapter<HelpAdapter.Item>() {
private var helpItems = titleDescriptionMap.map { (key, value) -> HelpItem(key, value) }
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): Item = Item(parent.inflate(R.layout.item_help, false))
override fun onBindViewHolder(
holder: Item,
position: Int
) {
holder.bind(helpItems[position])
}
override fun getItemCount(): Int = helpItems.size
internal inner class Item(itemView: View) :
BaseViewHolder<HelpItem>(itemView) {
@SuppressWarnings("MagicNumber")
fun toggleDescriptionVisibility() {
if (item_help_description.visibility == View.GONE) {
ObjectAnimator.ofFloat(item_help_toggle_expand, "rotation", 0f, 180f).start()
item_help_description.expand()
} else {
ObjectAnimator.ofFloat(item_help_toggle_expand, "rotation", 180f, 360f).start()
item_help_description.collapse()
}
}
override fun bind(item: HelpItem) {
item_help_title.setOnClickListener { toggleDescriptionVisibility() }
item_help_toggle_expand.setOnClickListener { toggleDescriptionVisibility() }
item_help_description.text = item.description
item_help_title.text = item.title
}
}
}
class HelpItem(val title: String, val description: String)

View File

@ -1,490 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.main;
import android.Manifest;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.core.content.FileProvider;
import androidx.fragment.app.DialogFragment;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import javax.inject.Inject;
import kotlin.Unit;
import org.kiwix.kiwixmobile.core.CoreApp;
import org.kiwix.kiwixmobile.core.R;
import org.kiwix.kiwixmobile.core.R2;
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer;
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower;
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog;
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil;
/**
* 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.
*
* Notes are saved as text files at location: "{External Storage}/Kiwix/Notes/ZimFileName/ArticleUrl.txt"
*/
public class AddNoteDialog extends DialogFragment {
public static final String NOTES_DIRECTORY =
Environment.getExternalStorageDirectory() + "/Kiwix/Notes/";
public static final String TAG = "AddNoteDialog";
@BindView(R2.id.toolbar)
Toolbar toolbar; // Displays options for the note dialog
@BindView(R2.id.add_note_text_view)
TextView addNoteTextView; // Displays article title
@BindView(R2.id.add_note_edit_text)
EditText addNoteEditText; // Displays the note text
private Unbinder unbinder;
private String zimFileName;
private String zimFileTitle;
private String articleTitle;
// 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)
private String zimNotesDirectory; // Stores path to directory for the currently open zim's notes
@Inject SharedPreferenceUtil sharedPreferenceUtil;
@Inject ZimReaderContainer zimReaderContainer;
@Inject protected AlertDialogShower alertDialogShower;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
CoreApp.getCoreComponent()
.activityComponentBuilder()
.activity(getActivity())
.build()
.inject(this);
// Returns name of the form ".../Kiwix/granbluefantasy_en_all_all_nopic_2018-10.zim"
zimFileName = zimReaderContainer.getZimCanonicalPath();
if (zimFileName != null) { // No zim file currently opened
zimFileTitle = zimReaderContainer.getZimFileTitle();
articleTitle = ((WebViewProvider) getActivity()).getCurrentWebView().getTitle();
// Corresponds to "ZimFileName" of "{External Storage}/Kiwix/Notes/ZimFileName/ArticleUrl.txt"
String zimNoteDirectoryName = getZimNoteDirectoryName();
articleNotefileName = getArticleNotefileName();
zimNotesDirectory = NOTES_DIRECTORY + zimNoteDirectoryName + "/";
} else {
onFailureToCreateAddNoteDialog();
}
}
private void onFailureToCreateAddNoteDialog() {
showToast(R.string.error_file_not_found, Toast.LENGTH_LONG);
closeKeyboard();
getFragmentManager().beginTransaction().remove(this).commit();
}
@Override
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);
toolbar.setTitle(R.string.note);
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp);
toolbar.setNavigationOnClickListener(v -> {
exitAddNoteDialog();
closeKeyboard();
});
toolbar.setOnMenuItemClickListener(item -> {
int itemId = item.getItemId();
if (itemId == R.id.share_note) { // Opens app-chooser for sharing the note text file
shareNote();
} else if (itemId == R.id.save_note) { // Saves the note as a text file
saveNote(addNoteEditText.getText().toString());
} else if (itemId == R.id.delete_note) {
deleteNote();
}
return true;
});
toolbar.inflateMenu(R.menu.menu_add_note_dialog);
// 'Share' disabled for empty notes, 'Save' disabled for unedited notes
disableMenuItems();
addNoteTextView.setText(articleTitle);
// Show the previously saved note if it exists
displayNote();
addNoteEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
noteEdited = true;
enableSaveNoteMenuItem();
enableShareNoteMenuItem();
}
@Override
public void afterTextChanged(Editable s) {
}
});
return view;
}
private @NonNull String getZimNoteDirectoryName() {
String noteDirectoryName = getTextAfterLastSlashWithoutExtension(zimFileName);
return (!noteDirectoryName.isEmpty()) ? noteDirectoryName : zimFileTitle;
}
private @NonNull String getArticleNotefileName() {
// Returns url of the form: "content://org.kiwix.kiwixmobile.zim.base/A/Main_Page.html"
String articleUrl = ((WebViewProvider) getActivity()).getCurrentWebView().getUrl();
String notefileName = "";
if (articleUrl == null) {
onFailureToCreateAddNoteDialog();
} else {
notefileName = getTextAfterLastSlashWithoutExtension(articleUrl);
}
return (!notefileName.isEmpty()) ? notefileName : articleTitle;
}
private @NonNull String getTextAfterLastSlashWithoutExtension(@NonNull String path) {
/* That's about exactly what it does.
*
* From ".../Kiwix/granbluefantasy_en_all_all_nopic_2018-10.zim", returns "granbluefantasy_en_all_all_nopic_2018-10"
* From "content://org.kiwix.kiwixmobile.zim.base/A/Main_Page.html", returns "Main_Page"
* For null input or on being unable to find required text, returns null
* */
int rightmostSlash = path.lastIndexOf('/');
int rightmostDot = path.lastIndexOf('.');
if (rightmostSlash > -1 && rightmostDot > -1) {
return path.substring(
rightmostSlash + 1, (rightmostDot > rightmostSlash) ? rightmostDot : path.length());
}
return ""; // If couldn't find a dot and/or slash in the url
}
// Override onBackPressed() to respond to user pressing 'Back' button on navigation bar
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
return new Dialog(getActivity(), getTheme()) {
@Override
public void onBackPressed() {
exitAddNoteDialog();
}
};
}
private void exitAddNoteDialog() {
if (noteEdited) {
alertDialogShower.show(KiwixDialog.NotesDiscardConfirmation.INSTANCE, () -> {
dismissAddNoteDialog();
return Unit.INSTANCE;
});
} else {
// Closing unedited note dialog straightaway
dismissAddNoteDialog();
}
}
private void disableMenuItems() {
if (toolbar.getMenu() != null) {
MenuItem saveItem = toolbar.getMenu().findItem(R.id.save_note);
MenuItem shareItem = toolbar.getMenu().findItem(R.id.share_note);
MenuItem deleteItem = toolbar.getMenu().findItem(R.id.delete_note);
saveItem.setEnabled(false);
shareItem.setEnabled(false);
deleteItem.setEnabled(false);
saveItem.getIcon().setAlpha(130);
shareItem.getIcon().setAlpha(130);
deleteItem.getIcon().setAlpha(130);
} else {
Log.d(TAG, "Toolbar without inflated menu");
}
}
private 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 enableDeleteNoteMenuItem() {
if (toolbar.getMenu() != null) {
MenuItem deleteItem = toolbar.getMenu().findItem(R.id.delete_note);
deleteItem.setEnabled(true);
deleteItem.getIcon().setAlpha(255);
} else {
Log.d(TAG, "Toolbar without inflated menu");
}
}
private 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");
}
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (!noteFileExists) {
// Prepare for input in case of empty/new note
addNoteEditText.requestFocus();
showKeyboard();
}
}
private void showKeyboard() {
InputMethodManager inputMethodManager =
(InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
}
private void closeKeyboard() {
InputMethodManager inputMethodManager =
(InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.toggleSoftInput(InputMethodManager.HIDE_IMPLICIT_ONLY, 0);
}
private 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 (CoreApp.getInstance().isExternalStorageWritable()) {
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;
}
File notesFolder = new File(zimNotesDirectory);
boolean folderExists = true;
if (!notesFolder.exists()) {
// Try creating folder if it doesn't exist
folderExists = notesFolder.mkdirs();
}
if (folderExists) {
File noteFile = new File(notesFolder.getAbsolutePath(), articleNotefileName + ".txt");
// Save note text-file code:
try {
FileOutputStream fileOutputStream = new FileOutputStream(noteFile);
fileOutputStream.write(noteText.getBytes());
fileOutputStream.close();
showToast(R.string.note_save_successful, Toast.LENGTH_SHORT);
noteEdited = false; // As no unsaved changes remain
enableDeleteNoteMenuItem();
} 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 {
showToast(R.string.note_save_error_storage_not_writable, Toast.LENGTH_LONG);
}
}
private void deleteNote() {
File notesFolder = new File(zimNotesDirectory);
File noteFile = new File(notesFolder.getAbsolutePath(), articleNotefileName + ".txt");
boolean noteDeleted = noteFile.delete();
if (noteDeleted) {
addNoteEditText.getText().clear();
disableMenuItems();
showToast(R.string.note_delete_successful, Toast.LENGTH_LONG);
} else {
showToast(R.string.note_delete_unsuccessful, Toast.LENGTH_LONG);
}
}
private void displayNote() {
/* String content of the note text file given at:
* "{External Storage}/Kiwix/Notes/ZimFileTitle/ArticleTitle.txt"
* is displayed in the EditText field (note content area)
* */
File noteFile = new File(zimNotesDirectory + articleNotefileName + ".txt");
if (noteFile.exists()) {
noteFileExists = true;
StringBuilder contents = new StringBuilder();
try (BufferedReader input = new BufferedReader(new java.io.FileReader(noteFile))) {
String line;
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");
}
addNoteEditText.setText(contents.toString()); // Display the note content
addNoteEditText.setSelection(addNoteEditText.getText().length() - 1);
enableShareNoteMenuItem(); // As note content exists which can be shared
enableDeleteNoteMenuItem();
}
// No action in case the note file for the currently open article doesn't exist
}
private 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
}
File noteFile = new File(zimNotesDirectory + articleNotefileName + ".txt");
Uri noteFileUri = null;
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(), getContext().getPackageName() + ".fileprovider",
noteFile);
} else {
noteFileUri = Uri.fromFile(noteFile);
}
} else {
showToast(R.string.note_share_error_file_missing, Toast.LENGTH_SHORT);
}
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));
if (noteFileShareIntent.resolveActivity(getActivity().getPackageManager()) != null) {
startActivity(shareChooser);
}
}
}
private void showToast(int stringResource, int duration) {
Toast.makeText(getActivity(), stringResource, duration).show();
}
private void dismissAddNoteDialog() {
Dialog dialog = getDialog();
if (dialog != null) {
dialog.dismiss();
}
closeKeyboard();
}
@Override
public void onStart() {
super.onStart();
Dialog dialog = getDialog();
if (dialog != null) {
int width = ViewGroup.LayoutParams.MATCH_PARENT;
int height = ViewGroup.LayoutParams.MATCH_PARENT;
dialog.getWindow().setLayout(width, height);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
if (unbinder != null) {
unbinder.unbind();
}
}
}

View File

@ -0,0 +1,413 @@
/*
* 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.main
import android.Manifest
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.fragment.app.DialogFragment
import kotlinx.android.synthetic.main.dialog_add_note.add_note_edit_text
import kotlinx.android.synthetic.main.dialog_add_note.add_note_text_view
import kotlinx.android.synthetic.main.layout_toolbar.toolbar
import org.kiwix.kiwixmobile.core.CoreApp.Companion.coreComponent
import org.kiwix.kiwixmobile.core.CoreApp.Companion.instance
import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.extensions.toast
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.SimpleTextWatcher
import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog
import java.io.File
import java.io.IOException
import javax.inject.Inject
/**
* 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.
*
* Notes are saved as text files at location: "{External Storage}/Kiwix/Notes/ZimFileName/ArticleUrl.txt"
*/
// constant
const val DISABLE_ICON_ITEM_ALPHA = 130
const val ENABLE_ICON_ITEM_ALPHA = 255
class AddNoteDialog : DialogFragment() {
private var zimFileName: String? = null
private var zimFileTitle: String? = null
private var articleTitle: String? = null
// Corresponds to "ArticleUrl" of "{External Storage}/Kiwix/Notes/ZimFileName/ArticleUrl.txt"
private lateinit var articleNoteFileName: String
private var noteFileExists = false
private var noteEdited = false
private lateinit var root: View
// Keeps track of state of the note (whether edited since last save)
// Stores path to directory for the currently open zim's notes
private var zimNotesDirectory: String? = null
@Inject
lateinit var sharedPreferenceUtil: SharedPreferenceUtil
@Inject
lateinit var zimReaderContainer: ZimReaderContainer
@Inject
lateinit var alertDialogShower: AlertDialogShower
private val saveItem by lazy { toolbar.menu.findItem(R.id.save_note) }
private val shareItem by lazy { toolbar.menu.findItem(R.id.share_note) }
private val deleteItem by lazy { toolbar.menu.findItem(R.id.delete_note) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
coreComponent
.activityComponentBuilder()
.activity(requireActivity())
.build()
.inject(this)
// Returns name of the form ".../Kiwix/granbluefantasy_en_all_all_nopic_2018-10.zim"
zimFileName = zimReaderContainer.zimCanonicalPath
if (zimFileName != null) { // No zim file currently opened
zimFileTitle = zimReaderContainer.zimFileTitle
articleTitle = (activity as WebViewProvider?)?.getCurrentWebView()?.title
// Corresponds to "ZimFileName" of "{External Storage}/Kiwix/Notes/ZimFileName/ArticleUrl.txt"
articleNoteFileName = getArticleNotefileName()
zimNotesDirectory = "$NOTES_DIRECTORY$zimNoteDirectoryName/"
} else {
onFailureToCreateAddNoteDialog()
}
}
private fun onFailureToCreateAddNoteDialog() {
context.toast(R.string.error_file_not_found, Toast.LENGTH_LONG)
closeKeyboard()
requireFragmentManager().beginTransaction().remove(this).commit()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
root = inflater.inflate(R.layout.dialog_add_note, container, false)
return root
}
private val zimNoteDirectoryName: String
get() {
val noteDirectoryName = getTextAfterLastSlashWithoutExtension(zimFileName ?: "")
return (if (noteDirectoryName.isNotEmpty()) noteDirectoryName else zimFileTitle) ?: ""
}
private fun getArticleNotefileName(): String {
// Returns url of the form: "content://org.kiwix.kiwixmobile.zim.base/A/Main_Page.html"
val articleUrl = (activity as WebViewProvider?)?.getCurrentWebView()?.url
var noteFileName = ""
if (articleUrl == null) {
onFailureToCreateAddNoteDialog()
} else {
noteFileName = getTextAfterLastSlashWithoutExtension(articleUrl)
}
return (if (noteFileName.isNotEmpty()) noteFileName else articleTitle) ?: ""
}
/* From ".../Kiwix/granbluefantasy_en_all_all_nopic_2018-10.zim", returns "granbluefantasy_en_all_all_nopic_2018-10"
* From "content://org.kiwix.kiwixmobile.zim.base/A/Main_Page.html", returns "Main_Page"
* For null input or on being unable to find required text, returns null
*/
private fun getTextAfterLastSlashWithoutExtension(path: String): String =
path.substringAfterLast('/', "").substringBeforeLast('.', "")
// Override onBackPressed() to respond to user pressing 'Back' button on navigation bar
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : Dialog(requireContext(), theme) {
override fun onBackPressed() {
exitAddNoteDialog()
}
}
}
private fun exitAddNoteDialog() {
if (noteEdited) {
alertDialogShower.show(KiwixDialog.NotesDiscardConfirmation, ::dismissAddNoteDialog)
} else {
// Closing unedited note dialog straightaway
dismissAddNoteDialog()
}
}
private fun disableMenuItems() {
if (toolbar.menu != null) {
saveItem.isEnabled = false
shareItem.isEnabled = false
deleteItem.isEnabled = false
saveItem.icon.alpha = DISABLE_ICON_ITEM_ALPHA
shareItem.icon.alpha = DISABLE_ICON_ITEM_ALPHA
deleteItem.icon.alpha = DISABLE_ICON_ITEM_ALPHA
} else {
Log.d(TAG, "Toolbar without inflated menu")
}
}
private fun enableSaveNoteMenuItem() {
if (toolbar.menu != null) {
saveItem.isEnabled = true
saveItem.icon.alpha = ENABLE_ICON_ITEM_ALPHA
} else {
Log.d(TAG, "Toolbar without inflated menu")
}
}
private fun enableDeleteNoteMenuItem() {
if (toolbar.menu != null) {
deleteItem.isEnabled = true
deleteItem.icon.alpha = ENABLE_ICON_ITEM_ALPHA
} else {
Log.d(TAG, "Toolbar without inflated menu")
}
}
private fun enableShareNoteMenuItem() {
if (toolbar.menu != null) {
shareItem.isEnabled = true
shareItem.icon.alpha = ENABLE_ICON_ITEM_ALPHA
} else {
Log.d(TAG, "Toolbar without inflated menu")
}
}
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
toolbar.setTitle(R.string.note)
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp)
toolbar.setNavigationOnClickListener {
exitAddNoteDialog()
closeKeyboard()
}
toolbar.setOnMenuItemClickListener { item: MenuItem ->
when (item.itemId) {
R.id.share_note -> shareNote()
R.id.save_note -> saveNote(add_note_edit_text.text.toString())
R.id.delete_note -> deleteNote()
}
true
}
toolbar.inflateMenu(R.menu.menu_add_note_dialog)
// 'Share' disabled for empty notes, 'Save' disabled for unedited notes
disableMenuItems()
add_note_text_view.text = articleTitle
// Show the previously saved note if it exists
displayNote()
add_note_edit_text.addTextChangedListener(SimpleTextWatcher { _, _, _, _ ->
noteEdited = true
enableSaveNoteMenuItem()
enableShareNoteMenuItem()
})
if (!noteFileExists) {
// Prepare for input in case of empty/new note
add_note_edit_text.requestFocus()
showKeyboard()
}
}
private fun showKeyboard() {
val inputMethodManager =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
}
private fun closeKeyboard() {
val inputMethodManager =
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.toggleSoftInput(
InputMethodManager.HIDE_IMPLICIT_ONLY,
0
)
}
private fun saveNote(noteText: String) {
/* String content of the EditText, given by noteText, is saved into the text file given by:
* "{External Storage}/Kiwix/Notes/ZimFileTitle/ArticleTitle.txt"
* */
if (instance.isExternalStorageWritable) {
if (ContextCompat.checkSelfPermission(
requireContext(),
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
Log.d(
TAG,
"WRITE_EXTERNAL_STORAGE permission not granted"
)
context.toast(R.string.note_save_unsuccessful, Toast.LENGTH_LONG)
return
}
val notesFolder = File(zimNotesDirectory)
var folderExists = true
if (!notesFolder.exists()) {
// Try creating folder if it doesn't exist
folderExists = notesFolder.mkdirs()
}
if (folderExists) {
val noteFile =
File(notesFolder.absolutePath, "$articleNoteFileName.txt")
// Save note text-file code:
try {
noteFile.writeText(noteText)
context.toast(R.string.note_save_successful, Toast.LENGTH_SHORT)
noteEdited = false // As no unsaved changes remain
enableDeleteNoteMenuItem()
} catch (e: IOException) {
e.printStackTrace()
.also { context.toast(R.string.note_save_unsuccessful, Toast.LENGTH_LONG) }
}
} else {
context.toast(R.string.note_save_successful, Toast.LENGTH_LONG)
Log.d(TAG, "Required folder doesn't exist")
}
} else {
context.toast(R.string.note_save_error_storage_not_writable, Toast.LENGTH_LONG)
}
}
private fun deleteNote() {
val notesFolder = File(zimNotesDirectory)
val noteFile =
File(notesFolder.absolutePath, "$articleNoteFileName.txt")
val noteDeleted = noteFile.delete()
if (noteDeleted) {
add_note_edit_text.text.clear()
disableMenuItems()
context.toast(R.string.note_delete_successful, Toast.LENGTH_LONG)
} else {
context.toast(R.string.note_delete_unsuccessful, Toast.LENGTH_LONG)
}
}
/* String content of the note text file given at:
* "{External Storage}/Kiwix/Notes/ZimFileTitle/ArticleTitle.txt"
* is displayed in the EditText field (note content area)
*/
private fun displayNote() {
val noteFile = File("$zimNotesDirectory$articleNoteFileName.txt")
if (noteFile.exists()) {
readNoteFromFile(noteFile)
}
// No action in case the note file for the currently open article doesn't exist
}
private fun readNoteFromFile(noteFile: File) {
noteFileExists = true
val contents = noteFile.readText()
add_note_edit_text.setText(contents) // Display the note content
add_note_edit_text.setSelection(add_note_edit_text.text.length - 1)
enableShareNoteMenuItem() // As note content exists which can be shared
enableDeleteNoteMenuItem()
}
private fun 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) {
// Save edited note before sharing the text file
saveNote(add_note_edit_text.text.toString())
}
val noteFile = File("$zimNotesDirectory$articleNoteFileName.txt")
if (noteFile.exists()) {
val noteFileUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// From Nougat 7 (API 24) access to files is shared temporarily with other apps
// Need to use FileProvider for the same
FileProvider.getUriForFile(
requireContext(), requireContext().packageName + ".fileprovider",
noteFile
)
} else {
Uri.fromFile(noteFile)
}
val noteFileShareIntent = Intent(Intent.ACTION_SEND).apply {
type = "application/octet-stream"
putExtra(Intent.EXTRA_STREAM, noteFileUri)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
}
val shareChooser = Intent.createChooser(
noteFileShareIntent,
getString(R.string.note_share_app_chooser_title)
)
if (noteFileShareIntent.resolveActivity(requireActivity().packageManager) != null) {
startActivity(shareChooser)
}
} else {
context.toast(R.string.note_share_error_file_missing, Toast.LENGTH_SHORT)
}
}
private fun dismissAddNoteDialog() {
dialog?.dismiss()
closeKeyboard()
}
override fun onStart() {
super.onStart()
dialog?.let {
it.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
}
}
companion object {
@JvmField val NOTES_DIRECTORY =
Environment.getExternalStorageDirectory().toString() + "/Kiwix/Notes/"
const val TAG = "AddNoteDialog"
}
}

View File

@ -112,13 +112,13 @@ import org.kiwix.kiwixmobile.core.reader.ZimFileReader;
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer;
import org.kiwix.kiwixmobile.core.search.SearchActivity;
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.SearchInPreviousScreen;
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower;
import org.kiwix.kiwixmobile.core.utils.ExternalLinkOpener;
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog;
import org.kiwix.kiwixmobile.core.utils.LanguageUtils;
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil;
import org.kiwix.kiwixmobile.core.utils.StyleUtils;
import org.kiwix.kiwixmobile.core.utils.UpdateUtils;
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower;
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog;
import org.kiwix.kiwixmobile.core.utils.files.FileUtils;
import static android.app.Activity.RESULT_CANCELED;
@ -408,7 +408,7 @@ public abstract class CoreReaderFragment extends BaseFragment
@Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_main, container, false);
View root = inflater.inflate(R.layout.fragment_reader, container, false);
unbinder = ButterKnife.bind(this, root);
return root;
}
@ -452,8 +452,9 @@ public abstract class CoreReaderFragment extends BaseFragment
private void setupDocumentParser() {
documentParser = new DocumentParser(new DocumentParser.SectionsListener() {
@Override
public void sectionsLoaded(String title, List<TableDrawerAdapter.DocumentSection> sections) {
public void sectionsLoaded(String title, List<? extends TableDrawerAdapter.DocumentSection> sections) {
if (isAdded()) {
documentSections.addAll(sections);
tableDrawerAdapter.setTitle(title);
@ -728,6 +729,10 @@ public abstract class CoreReaderFragment extends BaseFragment
tabCallback = null;
hideBackToTopTimer.cancel();
hideBackToTopTimer = null;
webViewList.clear();
actionBar = null;
mainMenu = null;
tabRecyclerView.setAdapter(null);
tableDrawerAdapter.setTableClickListener(null);
tableDrawerAdapter = null;
unbinder.unbind();
@ -1656,7 +1661,8 @@ public abstract class CoreReaderFragment extends BaseFragment
}
}
selectTab(currentTab);
webViewList.get(currentTab).loadUrl(UpdateUtils.reformatProviderUrl(urls.getString(currentTab)));
webViewList.get(currentTab)
.loadUrl(UpdateUtils.reformatProviderUrl(urls.getString(currentTab)));
getCurrentWebView().setScrollY(positions.getInt(currentTab));
} catch (JSONException e) {
Log.w(TAG_KIWIX, "Kiwix shared preferences corrupted", e);

View File

@ -1,88 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.main;
import android.os.Handler;
import android.os.Looper;
import android.webkit.JavascriptInterface;
import android.webkit.WebView;
import java.util.ArrayList;
import java.util.List;
import static org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.DocumentSection;
public class DocumentParser {
private String title;
private SectionsListener listener;
private List<TableDrawerAdapter.DocumentSection> sections;
DocumentParser(SectionsListener listener) {
this.listener = listener;
}
public void initInterface(WebView webView) {
webView.addJavascriptInterface(new ParserCallback(), "DocumentParser");
}
public interface SectionsListener {
void sectionsLoaded(String title, List<DocumentSection> sections);
void clearSections();
}
class ParserCallback {
@JavascriptInterface
@SuppressWarnings("unused")
public void parse(final String sectionTitle, final String element, final String id) {
if (element.equals("H1")) {
title = sectionTitle.trim();
return;
}
DocumentSection section = new DocumentSection();
section.title = sectionTitle.trim();
section.id = id;
int level;
try {
String character = element.substring(element.length() - 1);
level = Integer.parseInt(character);
} catch (NumberFormatException e) {
level = 0;
}
section.level = level;
sections.add(section);
}
@JavascriptInterface
@SuppressWarnings("unused")
public void start() {
title = "";
sections = new ArrayList<>();
new Handler(Looper.getMainLooper()).post(() -> listener.clearSections());
}
@JavascriptInterface
@SuppressWarnings("unused")
public void stop() {
List<DocumentSection> listToBeSentToMainThread = new ArrayList<>(sections);
new Handler(Looper.getMainLooper()).post(() ->
listener.sectionsLoaded(title, listToBeSentToMainThread));
}
}
}

View File

@ -0,0 +1,79 @@
/*
* Kiwix Android
* Copyright (c) 2020 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.main
import android.os.Handler
import android.os.Looper
import android.webkit.JavascriptInterface
import android.webkit.WebView
import kotlin.collections.List
import kotlin.collections.ArrayList
import org.kiwix.kiwixmobile.core.main.TableDrawerAdapter.DocumentSection
public class DocumentParser(private var listener: DocumentParser.SectionsListener) {
private var title: String = ""
private var sections = ArrayList<TableDrawerAdapter.DocumentSection>()
public fun initInterface(webView: WebView) {
webView.addJavascriptInterface(ParserCallback(), "DocumentParser")
}
public interface SectionsListener {
fun sectionsLoaded(title: String, sections: List<DocumentSection>)
fun clearSections()
}
inner class ParserCallback {
@JavascriptInterface
public fun parse(sectionTitle: String, element: String, id: String) {
if (element == "H1")
title = sectionTitle.trim()
else {
sections.add(DocumentSection().apply {
this.id = id
title = sectionTitle.trim()
level = element.takeLast(1).toIntOrNull() ?: 0
})
}
}
@JavascriptInterface
public fun start() {
title = ""
sections = ArrayList()
Handler(Looper.getMainLooper()).post(Runnable(listener::clearSections))
}
@JavascriptInterface
public fun stop() {
val listToBeSentToMainThread: List<DocumentSection> = ArrayList(sections)
Handler(Looper.getMainLooper()).post {
listener.sectionsLoaded(
title,
listToBeSentToMainThread
)
}
}
}
}

View File

@ -51,6 +51,15 @@ import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil;
this.toolbarView = toolbarView;
this.bottomBarView = bottomBarView;
this.sharedPreferenceUtil = sharedPreferenceUtil;
fixInitalScrollingIssue();
}
/**
* The webview needs to be scrolled with 0 to not be slightly hidden on startup.
* See https://github.com/kiwix/kiwix-android/issues/2304 for issue description.
*/
private void fixInitalScrollingIssue() {
moveToolbar(0);
}
private boolean moveToolbar(int scrollDelta) {

View File

@ -267,7 +267,7 @@ public abstract class CorePrefsFragment extends PreferenceFragmentCompat impleme
public void openFolderSelect() {
StorageSelectDialog dialogFragment = new StorageSelectDialog();
dialogFragment.setOnSelectAction(this::onStorageDeviceSelected);
dialogFragment.show(((AppCompatActivity) getActivity()).getSupportFragmentManager(),
dialogFragment.show(getActivity().getSupportFragmentManager(),
getResources().getString(R.string.pref_storage));
}
@ -277,13 +277,9 @@ public abstract class CorePrefsFragment extends PreferenceFragmentCompat impleme
);
sharedPreferenceUtil.putPrefStorage(storageDevice.getName());
if (storageDevice.isInternal()) {
findPreference(PREF_STORAGE).setTitle(getResources().getString(R.string.internal_storage));
sharedPreferenceUtil.putPrefStorageTitle(
getResources().getString(R.string.internal_storage));
findPreference(PREF_STORAGE).setTitle(getString(R.string.internal_storage));
} else {
findPreference(PREF_STORAGE).setTitle(getResources().getString(R.string.external_storage));
sharedPreferenceUtil.putPrefStorageTitle(
getResources().getString(R.string.external_storage));
findPreference(PREF_STORAGE).setTitle(getString(R.string.external_storage));
}
return Unit.INSTANCE;
}

View File

@ -50,7 +50,6 @@ public class SharedPreferenceUtil {
private static final String PREF_BACK_TO_TOP = "pref_backtotop";
private static final String PREF_FULLSCREEN = "pref_fullscreen";
private static final String PREF_NEW_TAB_BACKGROUND = "pref_newtab_background";
private static final String PREF_STORAGE_TITLE = "pref_selected_title";
private static final String PREF_EXTERNAL_LINK_POPUP = "pref_external_link_popup";
private static final String PREF_IS_FIRST_RUN = "isFirstRun";
private static final String PREF_SHOW_BOOKMARKS_ALL_BOOKS = "show_bookmarks_current_book";
@ -115,10 +114,6 @@ public class SharedPreferenceUtil {
: CoreApp.getInstance().getFilesDir().getPath(); // workaround for emulators
}
public String getPrefStorageTitle(String defaultTitle) {
return sharedPreferences.getString(PREF_STORAGE_TITLE, defaultTitle);
}
public void putPrefLanguage(String language) {
sharedPreferences.edit().putString(PREF_LANG, language).apply();
}
@ -131,10 +126,6 @@ public class SharedPreferenceUtil {
sharedPreferences.edit().putBoolean(PREF_WIFI_ONLY, wifiOnly).apply();
}
public void putPrefStorageTitle(String storageTitle) {
sharedPreferences.edit().putString(PREF_STORAGE_TITLE, storageTitle).apply();
}
public void putPrefStorage(String storage) {
sharedPreferences.edit().putString(PREF_STORAGE, storage).apply();
prefStorages.onNext(storage);

View File

@ -0,0 +1,38 @@
/*
* Kiwix Android
* Copyright (c) 2020 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.utils
import android.text.Editable
import android.text.TextWatcher
class SimpleTextWatcher(
private val onTextWatcherChangeAction: (CharSequence?, Int, Int, Int) -> Unit
) : TextWatcher {
@SuppressWarnings("EmptyFunctionBlock")
override fun afterTextChanged(p0: Editable?) {
}
@SuppressWarnings("EmptyFunctionBlock")
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onTextWatcherChangeAction.invoke(s, start, before, count)
}
}

View File

@ -1,55 +0,0 @@
/*
* Kiwix Android
* Copyright (c) 2019 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.utils.dialog;
import android.content.Context;
import android.content.SharedPreferences;
public class RateAppCounter {
private static final String MASTER_NAME = "visitCounter";
private static final String NOTHANKS_CLICKED = "clickedNoThanks";
private SharedPreferences visitCounter;
RateAppCounter(Context context) {
visitCounter = context.getSharedPreferences(MASTER_NAME, 0);
visitCounter = context.getSharedPreferences(NOTHANKS_CLICKED, 0);
}
public boolean getNoThanksState() {
return visitCounter.getBoolean(NOTHANKS_CLICKED, false);
}
public void setNoThanksState(boolean val) {
SharedPreferences.Editor CounterEditor = visitCounter.edit();
CounterEditor.putBoolean(NOTHANKS_CLICKED, val);
CounterEditor.apply();
}
public int getCount() {
return visitCounter.getInt("count", 0);
}
public void setCount(int count) {
SharedPreferences.Editor CounterEditor = visitCounter.edit();
CounterEditor.putInt("count", count);
CounterEditor.apply();
}
}

View File

@ -0,0 +1,48 @@
/*
* 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.utils.dialog
import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
class RateAppCounter internal constructor(context: Context) {
companion object {
private const val NO_THANKS_CLICKED = "clickedNoThanks"
private const val COUNT = "count"
}
private var ratingsPreferences: SharedPreferences =
context.getSharedPreferences(NO_THANKS_CLICKED, 0)
var noThanksState: Boolean
get() = ratingsPreferences.getBoolean(NO_THANKS_CLICKED, false)
set(value) {
ratingsPreferences.edit {
putBoolean(NO_THANKS_CLICKED, value)
}
}
var count: Int
get() = ratingsPreferences.getInt(COUNT, 0)
set(count) {
ratingsPreferences.edit {
putInt(COUNT, count)
}
}
}

View File

@ -144,6 +144,8 @@ object FileUtils {
}
} catch (ignore: SecurityException) {
null
} catch (ignore: NullPointerException) {
null
}
}

View File

@ -27,7 +27,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/main_fragment_content" />
<include layout="@layout/reader_fragment_content" />
<TextView
android:id="@+id/no_open_book_text"

View File

@ -3,6 +3,7 @@
* Bellayet
* Bodhisattwa
* Sohom Datta
* Titodutta
* আজিজ
* আফতাবুজ্জামান
* এম আবু সাঈদ
@ -129,4 +130,6 @@
<string name="tag_text_only">শুধুমাত্র পাঠ্য</string>
<string name="no_bookmarks">কোন বুকমার্ক নেই</string>
<string name="search_open_in_new_tab">নতুন ট্যাবে খুলুন</string>
<string name="open_library">উন্মুক্ত পাঠাগার</string>
<string name="tab_restored">ট্যাব পুনরুদ্ধার করা হয়েছে</string>
</resources>

View File

@ -155,4 +155,7 @@
<string name="go_to_settings" fuzzy="true">Gå til indstillinger</string>
<string name="no_bookmarks">Ingen bogmærker</string>
<string name="reader">Læser</string>
<string name="open_library">Åbn bibliotek</string>
<string name="open_drawer">Åbn skuffe</string>
<string name="close_drawer">Luk skuffe</string>
</resources>

View File

@ -2,6 +2,7 @@
<!-- Authors:
* 1233qwer1234qwer4
* DraconicDark
* Elliot
* FF11
* Killarnee
* Metalhead64
@ -20,7 +21,7 @@
<string name="menu_exit_full_screen">Vollbild beenden</string>
<string name="menu_read_aloud">Vorlesen</string>
<string name="menu_read_aloud_stop">Mit dem Vorlesen aufhören</string>
<string name="menu_support_kiwix">Spenden</string>
<string name="menu_support_kiwix">Kiwix unterstützen</string>
<string name="save_media">Medium speichern</string>
<string name="save_media_error">Beim Versuch, das Medium zu speichern, ist ein Fehler aufgetreten!</string>
<string name="save_media_saved">Medium als %s nach Android/media/org.kiwix…/ gespeichert</string>

View File

@ -2,6 +2,7 @@
<!-- Authors:
* Fitoschido
* Geraki
* Giorgos456
* Glavkos
* Nikosgranturismogt
* Norhorn
@ -143,4 +144,5 @@
<string name="delete_bookmarks">Διαγραφή όλων των σελιδοδεικτών;</string>
<string name="delete_selected_bookmarks">Διαγραφή επιλεγμένων σελιδοδεικτών;</string>
<string name="pref_text_zoom_title">Μεγέθυνση κειμένου</string>
<string name="reader">Αναγνώστης</string>
</resources>

View File

@ -258,4 +258,6 @@
<string name="diagnostic_report_message">Envíe todos los detalles siguientes para que podamos diagnosticar el problema.</string>
<string name="percentage">%d%%</string>
<string name="pref_text_zoom_title">Zoom de texto</string>
<string name="tab_restored">Pestaña restaurada</string>
<string name="close_drawer">Cerrar cajón</string>
</resources>

View File

@ -5,6 +5,7 @@
* FarsiNevis
* Fatemi127
* Reza1615
* Sunfyre
-->
<resources>
<string name="menu_help">راهنما</string>
@ -162,4 +163,5 @@
<string name="auto">خودکار</string>
<string name="send_report">ارسال گزارش تشخیصی</string>
<string name="diagnostic_report">گزارش تشخیصی</string>
<string name="open_library">کتابخانهٔ آزاد</string>
</resources>

View File

@ -31,6 +31,7 @@
<string name="hotspot_failed_title">Hotspot gagal dihidupkan</string>
<string name="hotspot_failed_message">Tampaknya hotspot Anda telah hidup. Silakan memutuskan koneksi hotspot wifi Anda untuk lanjut.</string>
<string name="go_to_wifi_settings_label">Ke pengaturan WIFI</string>
<string name="no_books_selected_toast_message">Silakan pilih buku terlebih dahulu</string>
<string name="server_failed_message">Server tidak dapat dihidupkan. Silakan menyalakan hotspot Anda.</string>
<string name="server_failed_toast_message">Server tidak dapat dinyalakan.</string>
<string name="server_started_successfully_toast_message">Server berhasil dinyalakan.</string>
@ -41,6 +42,8 @@
<string name="wifi_dialog_title">Koneksi Wifi telah terdeteksi</string>
<string name="wifi_dialog_body">Untuk dapat melihat buku dengan alat elektronik lainnya, pastikan semua alat elektronik Anda terhubung ke jaringan Wifi yang sama.</string>
<string name="hotspot_dialog_neutral_button">LANJUT</string>
<string name="start_server_label">Nyalakan server</string>
<string name="stop_server_label">Matikan server</string>
<string name="server_started_message">Masukkan alamat IP ini ke browser Anda untuk mengakses server %s</string>
<string name="error_file_not_found">Galat: Berkas ZIM yang dipilih tidak ditemukan.</string>
<string name="zim_not_opened">Zim file tidak dapat dibuka</string>

View File

@ -91,7 +91,7 @@
<string name="got_it">Wszystko jasne</string>
<string name="did_you_know">Czy wiesz, że?</string>
<string name="undo">Cofnij</string>
<string name="tab_closed">Karta zamknięta</string>
<string name="tab_closed">Zakładka zamknięta</string>
<string name="bookmark_added">Dodano zakładkę</string>
<string name="rate_dialog_title">Prosimy nas ocenić</string>
<string name="rate_dialog_msg_1">Jeśli podoba ci się korzystanie z</string>
@ -109,7 +109,7 @@
<string name="speech_prompt_text">Mów by wyszukać %s</string>
<string name="speech_not_supported">Przepraszamy! Twoje urządzenie nie obsługuje wprowadzania głosowego</string>
<string name="local_zims">Urządzenie</string>
<string name="remote_zims">Dostępny</string>
<string name="remote_zims">Dostępne</string>
<string name="library">Biblioteka</string>
<string name="delete_zim_body">Następujące pliki zim zostaną usunięte:\n\n%s</string>
<string name="delete_zims_toast">Plik został usunięty</string>
@ -138,7 +138,7 @@
<string name="tts_resume">Podsumuj</string>
<string name="stop">stop</string>
<string name="internal_storage">Wewnętrzny</string>
<string name="external_storage">Zewnętrzne</string>
<string name="external_storage">Zewnętrzny</string>
<string name="yes">Tak</string>
<string name="no">Nie</string>
<string name="confirm_stop_download_title">Zatrzymać pobieranie?</string>
@ -151,10 +151,10 @@
<string name="next">Następny</string>
<string name="previous">Poprzednia</string>
<string name="wifi_only_title">Zezwolić na pobieranie zawartości przez sieć komórkową?</string>
<string name="wifi_only_msg">eśli wybierzesz „Tak”, nie będziesz ostrzegany w przyszłości. Jednak zawsze możesz to zmienić w Ustawieniach</string>
<string name="pref_wifi_only">Pobierz zawartość tylko poprzez WiFi</string>
<string name="wifi_only_msg">Jeśli wybierzesz „Tak”, nie będziesz ostrzegany w przyszłości. Jednak zawsze możesz to zmienić w Ustawieniach</string>
<string name="pref_wifi_only">Pobierz zasób tylko poprzez WiFi</string>
<string name="time_day">dzień</string>
<string name="time_hour">h</string>
<string name="time_hour">g</string>
<string name="time_minute">min</string>
<string name="time_second">s</string>
<string name="time_left">do lewej</string>
@ -177,8 +177,8 @@
<string name="crash_checkbox_device">Szczegóły urządzenia</string>
<string name="crash_button_confirm">WYŚLIJ SZCZEGÓŁY</string>
<string name="shortcut_disabled_message">Skrót niedostępny</string>
<string name="new_tab_shortcut_label">Nowa karta</string>
<string name="get_content_shortcut_label">Pobierz zawartość</string>
<string name="new_tab_shortcut_label">Nowa zakładka</string>
<string name="get_content_shortcut_label">Pobierz zasób</string>
<string name="fav_icon">Favicon</string>
<string name="articleCount">%s artykułów</string>
<string name="get_started">Rozpocznij</string>
@ -253,6 +253,7 @@
<string name="status">Status</string>
<string name="pref_clear_all_notes_summary">Usuwa wszystkie notatki ze wszystkich artykułów</string>
<string name="pref_clear_all_notes_title">Wyczyść wszystkie notatki</string>
<string name="pref_text_zoom_summary">Zmień rozmiar tekstu co 25%.</string>
<string name="tag_pic">Fotka</string>
<string name="tag_vid">Wideo</string>
<string name="tag_text_only">Tylko tekst</string>
@ -264,8 +265,10 @@
<string name="no_bookmarks">Brak zakładek</string>
<string name="no_history">Nie ma Historii</string>
<string name="device_default">Domyślne urządzenie</string>
<string name="delete_history" fuzzy="true">Usunąć historię?</string>
<string name="delete_bookmarks" fuzzy="true">Usunąć zakładki?</string>
<string name="delete_history">Usunąć całą historię?</string>
<string name="delete_selected_history">Usunąć wybraną historię?</string>
<string name="delete_bookmarks">Usunąć wszystkie zakładki?</string>
<string name="delete_selected_bookmarks">Usunąć wybrane zakładki?</string>
<string name="on">Włącz</string>
<string name="off">Wyłącz</string>
<string name="auto">Automatycznie</string>
@ -275,7 +278,11 @@
<string name="diagnostic_report_message">Prześlij wszystkie poniższe informacje, abyśmy mogli zdiagnozować problem</string>
<string name="percentage">%d%%</string>
<string name="pref_text_zoom_title">Powiększ tekst</string>
<string name="search_open_in_new_tab">Otwórz w nowej karcie</string>
<string name="reader">Czytnik</string>
<string name="no_open_book">Brak otwartych książek</string>
<string name="open_library">Otwórz bibliotekę</string>
<string name="tab_restored">Odtworzono zakładkę</string>
<string name="open_drawer">Otwórz szufladę</string>
<string name="close_drawer">Zamknij szufladę</string>
</resources>

View File

@ -275,6 +275,10 @@
<string name="percentage">%d%%</string>
<string name="pref_text_zoom_title">Zoom de texto</string>
<string name="search_open_in_new_tab">Abrir em uma nova guia</string>
<string name="reader">Leitor</string>
<string name="no_open_book">Sem livro aberto</string>
<string name="open_library">Biblioteca aberta</string>
<string name="tab_restored">Guia restaurada</string>
<string name="open_drawer">Abrir gaveta</string>
<string name="close_drawer">Fechar gaveta</string>
</resources>

View File

@ -263,4 +263,5 @@
<string name="percentage">%d%%</string>
<string name="pref_text_zoom_title">Text-zoomning</string>
<string name="search_open_in_new_tab">Öppna i ny flik</string>
<string name="tab_restored">Fliken återställdes</string>
</resources>

View File

@ -200,4 +200,5 @@
<string name="delete_bookmarks">要删除所有书签吗?</string>
<string name="delete_selected_bookmarks">要删除所选的书签吗?</string>
<string name="search_open_in_new_tab">在新标签页打开</string>
<string name="reader">读者</string>
</resources>