mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-15 10:26:53 -04:00
merge with develop
This commit is contained in:
commit
1385baf348
@ -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
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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()
|
||||
}
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -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.
|
@ -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 =
|
@ -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
|
@ -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
|
||||
}
|
||||
}
|
@ -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());
|
@ -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");
|
@ -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.
|
@ -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()
|
||||
}
|
@ -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
|
@ -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
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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))
|
||||
}
|
@ -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>
|
@ -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" />
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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" />
|
||||
|
@ -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
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -144,6 +144,8 @@ object FileUtils {
|
||||
}
|
||||
} catch (ignore: SecurityException) {
|
||||
null
|
||||
} catch (ignore: NullPointerException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user