diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d3500a948..16bfe788b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,7 +52,7 @@ Branch names should be in the format **\-kebab-case-title** All branches should have distinct history and should be visually easy to follow, for this reason only 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 diff --git a/app/detekt_baseline.xml b/app/detekt_baseline.xml index f8790219c..083a17154 100644 --- a/app/detekt_baseline.xml +++ b/app/detekt_baseline.xml @@ -38,10 +38,10 @@ PackageNaming:ShareFiles.kt$package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects PackageNaming:SimplePageChangeListener.kt$package org.kiwix.kiwixmobile.zim_manager PackageNaming:StartMultiSelection.kt$package org.kiwix.kiwixmobile.zim_manager.fileselect_view.effects - PackageNaming:WifiDirectManager.kt$package org.kiwix.kiwixmobile.local_file_transfer - PackageNaming:WifiP2pDelegate.kt$package org.kiwix.kiwixmobile.local_file_transfer.adapter - PackageNaming:WifiP2pViewHolder.kt$package org.kiwix.kiwixmobile.local_file_transfer.adapter - PackageNaming:WifiPeerListAdapter.kt$package org.kiwix.kiwixmobile.local_file_transfer.adapter + PackageNaming:WifiDirectManager.kt$package org.kiwix.kiwixmobile.localFileTransfer + PackageNaming:WifiP2pDelegate.kt$package org.kiwix.kiwixmobile.localFileTransfer.adapter + PackageNaming:WifiP2pViewHolder.kt$package org.kiwix.kiwixmobile.localFileTransfer.adapter + PackageNaming:WifiPeerListAdapter.kt$package org.kiwix.kiwixmobile.localFileTransfer.adapter PackageNaming:ZimFileSelectFragment.kt$package org.kiwix.kiwixmobile.zim_manager.fileselect_view PackageNaming:ZimManageActivity.kt$package org.kiwix.kiwixmobile.zim_manager PackageNaming:ZimManageViewModel.kt$package org.kiwix.kiwixmobile.zim_manager diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt new file mode 100644 index 000000000..2e5330ed4 --- /dev/null +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/settings/SettingsRobot.kt @@ -0,0 +1,133 @@ +/* + * Kiwix Android + * Copyright (c) 2020 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.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( + 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 = + context.resources.getStringArray(R.array.pref_night_modes_entries) + + private fun IntArray.matchers() = map(::withText).toTypedArray() +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 34d27c4e4..8e9f746b7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -133,7 +133,7 @@ diff --git a/app/src/main/java/org/kiwix/kiwixmobile/di/components/KiwixActivityComponent.kt b/app/src/main/java/org/kiwix/kiwixmobile/di/components/KiwixActivityComponent.kt index 133fa555d..36630312a 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/di/components/KiwixActivityComponent.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/di/components/KiwixActivityComponent.kt @@ -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) diff --git a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/FileItem.kt b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/FileItem.kt similarity index 90% rename from app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/FileItem.kt rename to app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/FileItem.kt index 4f2c05687..c9aeb6aec 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/FileItem.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/FileItem.kt @@ -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. diff --git a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/FileListAdapter.kt b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/FileListAdapter.kt similarity index 85% rename from app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/FileListAdapter.kt rename to app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/FileListAdapter.kt index 945ad254e..231538184 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/FileListAdapter.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/FileListAdapter.kt @@ -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) : +class FileListAdapter(private val fileItems: List) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder = diff --git a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/KiwixWifiP2pBroadcastReceiver.kt b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/KiwixWifiP2pBroadcastReceiver.kt similarity index 98% rename from app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/KiwixWifiP2pBroadcastReceiver.kt rename to app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/KiwixWifiP2pBroadcastReceiver.kt index a45ae0647..ad1b25117 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/KiwixWifiP2pBroadcastReceiver.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/KiwixWifiP2pBroadcastReceiver.kt @@ -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 diff --git a/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/LocalFileTransferActivity.kt b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/LocalFileTransferActivity.kt new file mode 100644 index 000000000..344637a2b --- /dev/null +++ b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/LocalFileTransferActivity.kt @@ -0,0 +1,378 @@ +/* + * Kiwix Android + * Copyright (c) 2019 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package org.kiwix.kiwixmobile.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? = + 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) { + displayFileTransferProgress(filesForTransfer) + } + + private fun displayFileTransferProgress(filesToSend: List) { + 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 = ArrayList(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, + 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 + } +} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/PeerGroupHandshakeAsyncTask.java b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/PeerGroupHandshakeAsyncTask.java similarity index 97% rename from app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/PeerGroupHandshakeAsyncTask.java rename to app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/PeerGroupHandshakeAsyncTask.java index 0376eacb5..c051adda3 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/PeerGroupHandshakeAsyncTask.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/PeerGroupHandshakeAsyncTask.java @@ -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 { // Send total number of files which will be transferred objectOutputStream.writeObject("" + wifiDirectManager.getTotalFilesForTransfer()); - ArrayList fileItemArrayList = wifiDirectManager.getFilesForTransfer(); + List 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()); diff --git a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/ReceiverDeviceAsyncTask.java b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/ReceiverDeviceAsyncTask.java similarity index 90% rename from app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/ReceiverDeviceAsyncTask.java rename to app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/ReceiverDeviceAsyncTask.java index 130960552..25997b1ba 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/ReceiverDeviceAsyncTask.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/ReceiverDeviceAsyncTask.java @@ -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 { Log.d(TAG, "Server: Socket opened at " + WifiDirectManager.FILE_TRANSFER_PORT); final String zimStorageRootPath = wifiDirectManager.getZimStorageRootPath(); - ArrayList fileItems = wifiDirectManager.getFilesForTransfer(); + List fileItems = wifiDirectManager.getFilesForTransfer(); boolean isTransferErrorFree = true; if (BuildConfig.DEBUG) Log.d(TAG, "Expecting " + fileItems.size() + " files"); diff --git a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/SenderDeviceAsyncTask.java b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/SenderDeviceAsyncTask.java similarity index 93% rename from app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/SenderDeviceAsyncTask.java rename to app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/SenderDeviceAsyncTask.java index 898b1dca8..7daf75d46 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/SenderDeviceAsyncTask.java +++ b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/SenderDeviceAsyncTask.java @@ -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. diff --git a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/WifiDirectManager.kt b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/WifiDirectManager.kt similarity index 96% rename from app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/WifiDirectManager.kt rename to app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/WifiDirectManager.kt index e1170ef5d..0c2307b5a 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/WifiDirectManager.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/WifiDirectManager.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . * */ -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 + private lateinit var filesForTransfer: List // 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) { + fun startWifiDirectManager(filesForTransfer: List) { 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) + fun onFilesForTransferAvailable(filesForTransfer: List) fun onFileStatusChanged(itemIndex: Int) fun onFileTransferComplete() } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/adapter/WifiP2pDelegate.kt b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/adapter/WifiP2pDelegate.kt similarity index 96% rename from app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/adapter/WifiP2pDelegate.kt rename to app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/adapter/WifiP2pDelegate.kt index 199707e9a..557839e94 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/adapter/WifiP2pDelegate.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/adapter/WifiP2pDelegate.kt @@ -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 diff --git a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/adapter/WifiP2pViewHolder.kt b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/adapter/WifiP2pViewHolder.kt similarity index 95% rename from app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/adapter/WifiP2pViewHolder.kt rename to app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/adapter/WifiP2pViewHolder.kt index cfaf8748e..f05bce1ce 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/adapter/WifiP2pViewHolder.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/adapter/WifiP2pViewHolder.kt @@ -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 diff --git a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/adapter/WifiPeerListAdapter.kt b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/adapter/WifiPeerListAdapter.kt similarity index 94% rename from app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/adapter/WifiPeerListAdapter.kt rename to app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/adapter/WifiPeerListAdapter.kt index 0b496b2c6..28dfc1dc7 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/adapter/WifiPeerListAdapter.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/localFileTransfer/adapter/WifiPeerListAdapter.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . * */ -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 diff --git a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/LocalFileTransferActivity.java b/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/LocalFileTransferActivity.java deleted file mode 100644 index 4f5d53903..000000000 --- a/app/src/main/java/org/kiwix/kiwixmobile/local_file_transfer/LocalFileTransferActivity.java +++ /dev/null @@ -1,406 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2019 Kiwix - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package org.kiwix.kiwixmobile.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 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 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 filesForTransfer) { - this.filesForTransfer = filesForTransfer; - displayFileTransferProgress(filesForTransfer); - } - - private void displayFileTransferProgress(@NonNull ArrayList 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 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) () -> { - 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() { - @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() { - @Override public Unit invoke() { - startActivityForResult(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS), - REQUEST_ENABLE_LOCATION_SERVICES); - return Unit.INSTANCE; - } - }, - new Function0() { - @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() { - @Override public Unit invoke() { - startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS)); - return Unit.INSTANCE; - } - }, - new Function0() { - @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); - } -} diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryFragment.kt index a5c958edf..15fe572cd 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/LocalLibraryFragment.kt @@ -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 diff --git a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/OnlineLibraryFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/OnlineLibraryFragment.kt index 9af49affa..cb9cdfc19 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/OnlineLibraryFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/nav/destination/library/OnlineLibraryFragment.kt @@ -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(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?) { + 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.onCreateOptionsMenu(menu, inflater) + super.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)) + } } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixPrefsFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixPrefsFragment.kt index 226148190..576fe1356 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixPrefsFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/settings/KiwixPrefsFragment.kt @@ -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(PREF_STORAGE)?.title = - sharedPreferenceUtil.getPrefStorageTitle("Internal") - } else { - findPreference(PREF_STORAGE)?.title = - sharedPreferenceUtil.getPrefStorageTitle("External") - } + findPreference(PREF_STORAGE)?.title = getString( + if (sharedPreferenceUtil.prefStorage == internalStorage()) R.string.internal_storage + else R.string.external_storage + ) findPreference(PREF_STORAGE)?.summary = storageCalculator.calculateAvailableSpace() } + + private fun internalStorage(): String? = + ContextCompat.getExternalFilesDirs(requireContext(), null).firstOrNull()?.path } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/FileWritingFileSystemChecker.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/FileWritingFileSystemChecker.kt index 769ade285..cffd47a8d 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/FileWritingFileSystemChecker.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/FileWritingFileSystemChecker.kt @@ -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() { diff --git a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/LibraryFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/LibraryFragment.kt deleted file mode 100644 index 67a4dfd03..000000000 --- a/app/src/main/java/org/kiwix/kiwixmobile/zim_manager/library_view/LibraryFragment.kt +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2019 Kiwix - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package org.kiwix.kiwixmobile.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(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?) { - 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)) -} diff --git a/app/src/main/res/layout/activity_library.xml b/app/src/main/res/layout/activity_library.xml deleted file mode 100644 index 82f29f0dd..000000000 --- a/app/src/main/res/layout/activity_library.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/activity_local_file_transfer.xml b/app/src/main/res/layout/activity_local_file_transfer.xml index 8ba06a1b9..d579337c2 100644 --- a/app/src/main/res/layout/activity_local_file_transfer.xml +++ b/app/src/main/res/layout/activity_local_file_transfer.xml @@ -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"> diff --git a/app/src/main/res/layout/activity_zim_host.xml b/app/src/main/res/layout/activity_zim_host.xml index 2a7d085f4..ea4f0a0b8 100644 --- a/app/src/main/res/layout/activity_zim_host.xml +++ b/app/src/main/res/layout/activity_zim_host.xml @@ -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" diff --git a/app/src/main/res/menu/wifi_file_share_items.xml b/app/src/main/res/menu/wifi_file_share_items.xml index 3e8b199d5..ab64f5c02 100644 --- a/app/src/main/res/menu/wifi_file_share_items.xml +++ b/app/src/main/res/menu/wifi_file_share_items.xml @@ -2,7 +2,7 @@ + tools:context="org.kiwix.kiwixmobile.localFileTransfer.LocalFileTransferActivity"> + tools:targetApi="n"> + + + + + - diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/error/DiagnosticReportActivity.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/error/DiagnosticReportActivity.kt index 1c2c7d89d..b79abd161 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/error/DiagnosticReportActivity.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/error/DiagnosticReportActivity.kt @@ -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 diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/error/ErrorActivity.java b/core/src/main/java/org/kiwix/kiwixmobile/core/error/ErrorActivity.java deleted file mode 100644 index 9dac2af5b..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/error/ErrorActivity.java +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2019 Kiwix - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package org.kiwix.kiwixmobile.core.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 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); - } -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/error/ErrorActivity.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/error/ErrorActivity.kt new file mode 100644 index 000000000..8e3aa1da7 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/error/ErrorActivity.kt @@ -0,0 +1,225 @@ +/* + * Kiwix Android + * Copyright (c) 2019 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package org.kiwix.kiwixmobile.core.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) + } + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpAdapter.java b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpAdapter.java deleted file mode 100644 index 0d9d9b8e8..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpAdapter.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2019 Kiwix - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package org.kiwix.kiwixmobile.core.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 { - private final String[] titles; - private final String[] descriptions; - - HelpAdapter(Map 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); - } - } - } -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpAdapter.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpAdapter.kt new file mode 100644 index 000000000..32b33d99c --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/help/HelpAdapter.kt @@ -0,0 +1,73 @@ +/* + * Kiwix Android + * Copyright (c) 2019 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package org.kiwix.kiwixmobile.core.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) : + RecyclerView.Adapter() { + 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(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) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/AddNoteDialog.java b/core/src/main/java/org/kiwix/kiwixmobile/core/main/AddNoteDialog.java deleted file mode 100644 index 7112343a6..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/AddNoteDialog.java +++ /dev/null @@ -1,490 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2019 Kiwix - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package org.kiwix.kiwixmobile.core.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(); - } - } -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/AddNoteDialog.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/AddNoteDialog.kt new file mode 100644 index 000000000..11150a60a --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/AddNoteDialog.kt @@ -0,0 +1,413 @@ +/* + * Kiwix Android + * Copyright (c) 2019 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package org.kiwix.kiwixmobile.core.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" + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.java b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.java index 91439d739..cfa47c99a 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.java +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.java @@ -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 sections) { + public void sectionsLoaded(String title, List 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); diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/DocumentParser.java b/core/src/main/java/org/kiwix/kiwixmobile/core/main/DocumentParser.java deleted file mode 100644 index 3e0842c2d..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/DocumentParser.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2019 Kiwix - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ -package org.kiwix.kiwixmobile.core.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 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 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 listToBeSentToMainThread = new ArrayList<>(sections); - new Handler(Looper.getMainLooper()).post(() -> - listener.sectionsLoaded(title, listToBeSentToMainThread)); - } - } -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/DocumentParser.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/DocumentParser.kt new file mode 100644 index 000000000..0c0be6e07 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/DocumentParser.kt @@ -0,0 +1,79 @@ +/* + * Kiwix Android + * Copyright (c) 2020 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.core.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() + + public fun initInterface(webView: WebView) { + webView.addJavascriptInterface(ParserCallback(), "DocumentParser") + } + + public interface SectionsListener { + fun sectionsLoaded(title: String, sections: List) + + 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 = ArrayList(sections) + Handler(Looper.getMainLooper()).post { + listener.sectionsLoaded( + title, + listToBeSentToMainThread + ) + } + } + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/ToolbarScrollingKiwixWebView.java b/core/src/main/java/org/kiwix/kiwixmobile/core/main/ToolbarScrollingKiwixWebView.java index 714bc130a..11f5b65cd 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/ToolbarScrollingKiwixWebView.java +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/ToolbarScrollingKiwixWebView.java @@ -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) { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.java b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.java index 4182e159a..413b39c15 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.java +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.java @@ -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; } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.java b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.java index 894e46f3a..ee412a294 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.java +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SharedPreferenceUtil.java @@ -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); diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleTextWatcher.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleTextWatcher.kt new file mode 100644 index 000000000..c401569d7 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/SimpleTextWatcher.kt @@ -0,0 +1,38 @@ +/* + * Kiwix Android + * Copyright (c) 2020 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package org.kiwix.kiwixmobile.core.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) + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/RateAppCounter.java b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/RateAppCounter.java deleted file mode 100644 index c75e9d057..000000000 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/RateAppCounter.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Kiwix Android - * Copyright (c) 2019 Kiwix - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -package org.kiwix.kiwixmobile.core.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(); - } -} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/RateAppCounter.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/RateAppCounter.kt new file mode 100644 index 000000000..aa1775dc4 --- /dev/null +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/dialog/RateAppCounter.kt @@ -0,0 +1,48 @@ +/* + * Kiwix Android + * Copyright (c) 2019 Kiwix + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package org.kiwix.kiwixmobile.core.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) + } + } +} diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt index f095e4c9b..fcf6098ab 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt @@ -144,6 +144,8 @@ object FileUtils { } } catch (ignore: SecurityException) { null + } catch (ignore: NullPointerException) { + null } } diff --git a/core/src/main/res/layout/fragment_main.xml b/core/src/main/res/layout/fragment_reader.xml similarity index 97% rename from core/src/main/res/layout/fragment_main.xml rename to core/src/main/res/layout/fragment_reader.xml index e3095db65..2729270f9 100644 --- a/core/src/main/res/layout/fragment_main.xml +++ b/core/src/main/res/layout/fragment_reader.xml @@ -27,7 +27,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + শুধুমাত্র পাঠ্য কোন বুকমার্ক নেই নতুন ট্যাবে খুলুন + উন্মুক্ত পাঠাগার + ট্যাব পুনরুদ্ধার করা হয়েছে diff --git a/core/src/main/res/values-da/strings.xml b/core/src/main/res/values-da/strings.xml index b0d76d93c..9db731c2f 100644 --- a/core/src/main/res/values-da/strings.xml +++ b/core/src/main/res/values-da/strings.xml @@ -155,4 +155,7 @@ Gå til indstillinger Ingen bogmærker Læser + Åbn bibliotek + Åbn skuffe + Luk skuffe diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index 0cb22b716..0b933d6dd 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -2,6 +2,7 @@ راهنما @@ -162,4 +163,5 @@ خودکار ارسال گزارش تشخیصی گزارش تشخیصی + کتابخانهٔ آزاد diff --git a/core/src/main/res/values-in/strings.xml b/core/src/main/res/values-in/strings.xml index d7ced1329..765448e0c 100644 --- a/core/src/main/res/values-in/strings.xml +++ b/core/src/main/res/values-in/strings.xml @@ -31,6 +31,7 @@ Hotspot gagal dihidupkan Tampaknya hotspot Anda telah hidup. Silakan memutuskan koneksi hotspot wifi Anda untuk lanjut. Ke pengaturan WIFI + Silakan pilih buku terlebih dahulu Server tidak dapat dihidupkan. Silakan menyalakan hotspot Anda. Server tidak dapat dinyalakan. Server berhasil dinyalakan. @@ -41,6 +42,8 @@ Koneksi Wifi telah terdeteksi Untuk dapat melihat buku dengan alat elektronik lainnya, pastikan semua alat elektronik Anda terhubung ke jaringan Wifi yang sama. LANJUT + Nyalakan server + Matikan server Masukkan alamat IP ini ke browser Anda untuk mengakses server %s Galat: Berkas ZIM yang dipilih tidak ditemukan. Zim file tidak dapat dibuka diff --git a/core/src/main/res/values-pl/strings.xml b/core/src/main/res/values-pl/strings.xml index c9f0019f5..a776055a2 100644 --- a/core/src/main/res/values-pl/strings.xml +++ b/core/src/main/res/values-pl/strings.xml @@ -91,7 +91,7 @@ Wszystko jasne Czy wiesz, że? Cofnij - Karta zamknięta + Zakładka zamknięta Dodano zakładkę Prosimy nas ocenić Jeśli podoba ci się korzystanie z @@ -109,7 +109,7 @@ Mów by wyszukać %s Przepraszamy! Twoje urządzenie nie obsługuje wprowadzania głosowego Urządzenie - Dostępny + Dostępne Biblioteka Następujące pliki zim zostaną usunięte:\n\n%s Plik został usunięty @@ -138,7 +138,7 @@ Podsumuj stop Wewnętrzny - Zewnętrzne + Zewnętrzny Tak Nie Zatrzymać pobieranie? @@ -151,10 +151,10 @@ Następny Poprzednia Zezwolić na pobieranie zawartości przez sieć komórkową? - eśli wybierzesz „Tak”, nie będziesz ostrzegany w przyszłości. Jednak zawsze możesz to zmienić w Ustawieniach - Pobierz zawartość tylko poprzez WiFi + Jeśli wybierzesz „Tak”, nie będziesz ostrzegany w przyszłości. Jednak zawsze możesz to zmienić w Ustawieniach + Pobierz zasób tylko poprzez WiFi dzień - h + g min s do lewej @@ -177,8 +177,8 @@ Szczegóły urządzenia WYŚLIJ SZCZEGÓŁY Skrót niedostępny - Nowa karta - Pobierz zawartość + Nowa zakładka + Pobierz zasób Favicon %s artykułów Rozpocznij @@ -253,6 +253,7 @@ Status Usuwa wszystkie notatki ze wszystkich artykułów Wyczyść wszystkie notatki + Zmień rozmiar tekstu co 25%. Fotka Wideo Tylko tekst @@ -264,8 +265,10 @@ Brak zakładek Nie ma Historii Domyślne urządzenie - Usunąć historię? - Usunąć zakładki? + Usunąć całą historię? + Usunąć wybraną historię? + Usunąć wszystkie zakładki? + Usunąć wybrane zakładki? Włącz Wyłącz Automatycznie @@ -275,7 +278,11 @@ Prześlij wszystkie poniższe informacje, abyśmy mogli zdiagnozować problem %d%% Powiększ tekst + Otwórz w nowej karcie + Czytnik Brak otwartych książek Otwórz bibliotekę Odtworzono zakładkę + Otwórz szufladę + Zamknij szufladę diff --git a/core/src/main/res/values-pt-rBR/strings.xml b/core/src/main/res/values-pt-rBR/strings.xml index 0803a61c2..97f44cfb9 100644 --- a/core/src/main/res/values-pt-rBR/strings.xml +++ b/core/src/main/res/values-pt-rBR/strings.xml @@ -275,6 +275,10 @@ %d%% Zoom de texto Abrir em uma nova guia + Leitor + Sem livro aberto Biblioteca aberta + Guia restaurada Abrir gaveta + Fechar gaveta diff --git a/core/src/main/res/values-sv/strings.xml b/core/src/main/res/values-sv/strings.xml index 203b7eb73..9338e0e6e 100644 --- a/core/src/main/res/values-sv/strings.xml +++ b/core/src/main/res/values-sv/strings.xml @@ -263,4 +263,5 @@ %d%% Text-zoomning Öppna i ny flik + Fliken återställdes diff --git a/core/src/main/res/values-zh/strings.xml b/core/src/main/res/values-zh/strings.xml index b125659db..35c7e4c58 100644 --- a/core/src/main/res/values-zh/strings.xml +++ b/core/src/main/res/values-zh/strings.xml @@ -200,4 +200,5 @@ 要删除所有书签吗? 要删除所选的书签吗? 在新标签页打开 + 读者