forked from hardcoresushi/DroidFS
Multi volume openings
This commit is contained in:
parent
2d165c4a20
commit
1a1d3ea570
@ -88,9 +88,11 @@ dependencies {
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
implementation 'androidx.core:core-ktx:1.9.0'
|
||||
implementation "androidx.appcompat:appcompat:1.6.0"
|
||||
implementation "androidx.appcompat:appcompat:1.6.1"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
|
||||
def lifecycle_version = "2.5.1"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
|
||||
|
||||
implementation "androidx.sqlite:sqlite-ktx:2.3.0"
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
|
@ -30,7 +30,8 @@
|
||||
android:label="@string/app_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/BaseTheme">
|
||||
android:theme="@style/BaseTheme"
|
||||
android:name=".VolumeManagerApp">
|
||||
<activity android:name=".MainActivity" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
@ -4,7 +4,6 @@ import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
|
||||
open class BaseActivity: AppCompatActivity() {
|
||||
protected lateinit var sharedPrefs: SharedPreferences
|
||||
@ -13,7 +12,7 @@ open class BaseActivity: AppCompatActivity() {
|
||||
private var shouldCheckTheme = true
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
sharedPrefs = (application as VolumeManagerApp).sharedPreferences
|
||||
themeValue = sharedPrefs.getString(Constants.THEME_VALUE_KEY, Constants.DEFAULT_THEME_VALUE)!!
|
||||
if (shouldCheckTheme && applyCustomTheme) {
|
||||
when (themeValue) {
|
||||
|
@ -17,7 +17,6 @@ import android.view.animation.RotateAnimation
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.addCallback
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.camera.camera2.interop.Camera2CameraInfo
|
||||
import androidx.camera.core.*
|
||||
@ -29,7 +28,6 @@ import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
||||
import sushi.hardcore.droidfs.databinding.ActivityCameraBinding
|
||||
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
||||
import sushi.hardcore.droidfs.util.IntentUtils
|
||||
@ -63,14 +61,11 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
binding.imageTimer.setImageResource(R.drawable.icon_timer_off)
|
||||
}
|
||||
}
|
||||
private var usf_keep_open = false
|
||||
private lateinit var sensorOrientationListener: SensorOrientationListener
|
||||
private var previousOrientation: Float = 0f
|
||||
private lateinit var orientedIcons: List<ImageView>
|
||||
private lateinit var encryptedVolume: EncryptedVolume
|
||||
private lateinit var outputDirectory: String
|
||||
private var isFinishingIntentionally = false
|
||||
private var isAskingPermissions = false
|
||||
private var permissionsGranted = false
|
||||
private lateinit var executor: Executor
|
||||
private lateinit var cameraProvider: ProcessCameraProvider
|
||||
@ -92,7 +87,6 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
|
||||
binding = ActivityCameraBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.hide()
|
||||
@ -103,7 +97,6 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED){
|
||||
permissionsGranted = true
|
||||
} else {
|
||||
isAskingPermissions = true
|
||||
requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE)
|
||||
}
|
||||
} else {
|
||||
@ -220,7 +213,6 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
binding.takePhotoButton.visibility = View.GONE
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||
isAskingPermissions = true
|
||||
requestPermissions(arrayOf(Manifest.permission.RECORD_AUDIO), AUDIO_PERMISSION_REQUEST_CODE)
|
||||
}
|
||||
}
|
||||
@ -280,17 +272,11 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
isFinishingIntentionally = true
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
isAskingPermissions = false
|
||||
if (grantResults.size == 1) {
|
||||
when (requestCode) {
|
||||
CAMERA_PERMISSION_REQUEST_CODE -> if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
@ -301,10 +287,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.camera_perm_needed)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
isFinishingIntentionally = true
|
||||
finish()
|
||||
}.show()
|
||||
.setPositiveButton(R.string.ok) { _, _ -> finish() }.show()
|
||||
}
|
||||
AUDIO_PERMISSION_REQUEST_CODE -> if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
if (videoCapture != null) {
|
||||
@ -431,10 +414,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.picture_save_failed)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
isFinishingIntentionally = true
|
||||
finish()
|
||||
}
|
||||
.setPositiveButton(R.string.ok) { _, _ -> finish() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
@ -485,32 +465,18 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (!isFinishingIntentionally) {
|
||||
encryptedVolume.close()
|
||||
RestrictedFileProvider.wipeAll(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (!isFinishing && !usf_keep_open){
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
sensorOrientationListener.remove(this)
|
||||
if (!isAskingPermissions && !usf_keep_open) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
sensorOrientationListener.addListener(this)
|
||||
if (encryptedVolume.isClosed()) {
|
||||
finish()
|
||||
} else {
|
||||
sensorOrientationListener.addListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOrientationChange(newOrientation: Int) {
|
||||
|
@ -20,12 +20,10 @@ import kotlinx.coroutines.launch
|
||||
import sushi.hardcore.droidfs.Constants.DEFAULT_VOLUME_KEY
|
||||
import sushi.hardcore.droidfs.adapters.VolumeAdapter
|
||||
import sushi.hardcore.droidfs.add_volume.AddVolumeActivity
|
||||
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
||||
import sushi.hardcore.droidfs.databinding.ActivityMainBinding
|
||||
import sushi.hardcore.droidfs.databinding.DialogDeleteVolumeBinding
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerRouter
|
||||
import sushi.hardcore.droidfs.file_operations.FileOperationService
|
||||
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
||||
import sushi.hardcore.droidfs.util.IntentUtils
|
||||
import sushi.hardcore.droidfs.util.PathUtils
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
@ -33,11 +31,15 @@ import sushi.hardcore.droidfs.widgets.EditTextDialog
|
||||
import java.io.File
|
||||
|
||||
class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
companion object {
|
||||
private const val OPEN_DEFAULT_VOLUME = "openDefault"
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var volumeDatabase: VolumeDatabase
|
||||
private lateinit var volumeManager: VolumeManager
|
||||
private lateinit var volumeAdapter: VolumeAdapter
|
||||
private lateinit var volumeOpener: VolumeOpener
|
||||
private var usfKeepOpen: Boolean = false
|
||||
private var addVolume = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if ((explorerRouter.pickMode || explorerRouter.dropMode) && result.resultCode != AddVolumeActivity.RESULT_USER_BACK) {
|
||||
setResult(result.resultCode, result.data) // forward result
|
||||
@ -54,7 +56,6 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
}
|
||||
private lateinit var fileOperationService: FileOperationService
|
||||
private lateinit var explorerRouter: ExplorerRouter
|
||||
private var shouldCloseVolume = true // used when launched to pick file from another volume
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -80,10 +81,12 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
.show()
|
||||
}
|
||||
explorerRouter = ExplorerRouter(this, intent)
|
||||
volumeManager = (application as VolumeManagerApp).volumeManager
|
||||
volumeDatabase = VolumeDatabase(this)
|
||||
volumeAdapter = VolumeAdapter(
|
||||
this,
|
||||
volumeDatabase,
|
||||
(application as VolumeManagerApp).volumeManager,
|
||||
!explorerRouter.pickMode && !explorerRouter.dropMode,
|
||||
!explorerRouter.dropMode,
|
||||
this,
|
||||
@ -100,30 +103,31 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
addVolume.launch(Intent(this, AddVolumeActivity::class.java).also {
|
||||
if (explorerRouter.dropMode || explorerRouter.pickMode) {
|
||||
IntentUtils.forwardIntent(intent, it)
|
||||
shouldCloseVolume = false
|
||||
}
|
||||
})
|
||||
}
|
||||
usfKeepOpen = sharedPrefs.getBoolean("usf_keep_open", false)
|
||||
volumeOpener = VolumeOpener(this)
|
||||
volumeOpener.defaultVolumeName?.let { name ->
|
||||
try {
|
||||
openVolume(volumeAdapter.volumes.first { it.name == name })
|
||||
} catch (e: NoSuchElementException) {
|
||||
unsetDefaultVolume()
|
||||
}
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
if (volumeAdapter.selectedItems.isNotEmpty()) {
|
||||
unselectAll()
|
||||
} else {
|
||||
if (explorerRouter.pickMode) {
|
||||
shouldCloseVolume = false
|
||||
}
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
}
|
||||
volumeOpener.defaultVolumeName?.let { name ->
|
||||
val state = savedInstanceState?.getBoolean(OPEN_DEFAULT_VOLUME)
|
||||
if (state == true || state == null) {
|
||||
val volumeData = volumeAdapter.volumes.first { it.name == name }
|
||||
if (!volumeManager.isOpen(volumeData)) {
|
||||
try {
|
||||
openVolume(volumeData)
|
||||
} catch (e: NoSuchElementException) {
|
||||
unsetDefaultVolume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent(this, FileOperationService::class.java).also {
|
||||
bindService(it, object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
@ -148,6 +152,11 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
volumeOpener.defaultVolumeName = sharedPrefs.getString(DEFAULT_VOLUME_KEY, null)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putBoolean(OPEN_DEFAULT_VOLUME, false)
|
||||
}
|
||||
|
||||
override fun onSelectionChanged(size: Int) {
|
||||
title = if (size == 0) {
|
||||
getString(R.string.app_name)
|
||||
@ -179,6 +188,11 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
private fun removeVolume(volume: VolumeData) {
|
||||
volumeManager.getVolumeId(volume)?.let { volumeManager.closeVolume(it) }
|
||||
volumeDatabase.removeVolume(volume.name)
|
||||
}
|
||||
|
||||
private fun removeVolumes(volumes: List<VolumeData>, i: Int = 0, doDeleteVolumeContent: Boolean? = null) {
|
||||
if (i < volumes.size) {
|
||||
if (volumes[i].isHidden) {
|
||||
@ -196,12 +210,12 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
.setTitle(R.string.warning)
|
||||
.setView(dialogBinding.root)
|
||||
.setPositiveButton(R.string.forget_only) { _, _ ->
|
||||
volumeDatabase.removeVolume(volumes[i].name)
|
||||
removeVolume(volumes[i])
|
||||
removeVolumes(volumes, i + 1, if (dialogBinding.checkboxApplyToAll.isChecked) false else null)
|
||||
}
|
||||
.setNegativeButton(R.string.delete_volume) { _, _ ->
|
||||
PathUtils.recursiveRemoveDirectory(File(volumes[i].getFullPath(filesDir.path)))
|
||||
volumeDatabase.removeVolume(volumes[i].name)
|
||||
removeVolume(volumes[i])
|
||||
removeVolumes(volumes, i + 1, if (dialogBinding.checkboxApplyToAll.isChecked) true else null)
|
||||
}
|
||||
.setOnCancelListener {
|
||||
@ -213,11 +227,11 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
if (doDeleteVolumeContent) {
|
||||
PathUtils.recursiveRemoveDirectory(File(volumes[i].getFullPath(filesDir.path)))
|
||||
}
|
||||
volumeDatabase.removeVolume(volumes[i].name)
|
||||
removeVolume(volumes[i])
|
||||
removeVolumes(volumes, i + 1, doDeleteVolumeContent)
|
||||
}
|
||||
} else {
|
||||
volumeDatabase.removeVolume(volumes[i].name)
|
||||
removeVolume(volumes[i])
|
||||
removeVolumes(volumes, i + 1, doDeleteVolumeContent)
|
||||
}
|
||||
} else {
|
||||
@ -241,9 +255,6 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
return when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
if (explorerRouter.pickMode || explorerRouter.dropMode) {
|
||||
if (explorerRouter.pickMode) {
|
||||
shouldCloseVolume = false
|
||||
}
|
||||
finish()
|
||||
} else {
|
||||
unselectAll()
|
||||
@ -255,6 +266,15 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
invalidateOptionsMenu()
|
||||
true
|
||||
}
|
||||
R.id.lock -> {
|
||||
volumeAdapter.selectedItems.forEach {
|
||||
volumeManager.getVolumeId(volumeAdapter.volumes[it])?.let { id ->
|
||||
volumeManager.closeVolume(id)
|
||||
}
|
||||
}
|
||||
unselectAll()
|
||||
true
|
||||
}
|
||||
R.id.remove -> {
|
||||
val selectedVolumes = volumeAdapter.selectedItems.map { i -> volumeAdapter.volumes[i] }
|
||||
removeVolumes(selectedVolumes)
|
||||
@ -325,6 +345,9 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
menu.findItem(R.id.settings).isVisible = !explorerRouter.pickMode && !explorerRouter.dropMode
|
||||
val isSelecting = volumeAdapter.selectedItems.isNotEmpty()
|
||||
menu.findItem(R.id.select_all).isVisible = isSelecting
|
||||
menu.findItem(R.id.lock).isVisible = isSelecting && volumeAdapter.selectedItems.any {
|
||||
i -> volumeManager.isOpen(volumeAdapter.volumes[i])
|
||||
}
|
||||
menu.findItem(R.id.remove).isVisible = isSelecting
|
||||
menu.findItem(R.id.delete_password_hash).isVisible =
|
||||
isSelecting &&
|
||||
@ -456,11 +479,8 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
volumeAdapter.refresh()
|
||||
}
|
||||
|
||||
override fun onVolumeOpened(encryptedVolume: EncryptedVolume, volumeShortName: String) {
|
||||
startActivity(explorerRouter.getExplorerIntent(encryptedVolume, volumeShortName))
|
||||
if (explorerRouter.pickMode) {
|
||||
shouldCloseVolume = false
|
||||
}
|
||||
override fun onVolumeOpened(id: Int) {
|
||||
startActivity(explorerRouter.getExplorerIntent(id, volume.shortName))
|
||||
if (explorerRouter.dropMode || explorerRouter.pickMode) {
|
||||
finish()
|
||||
}
|
||||
@ -468,18 +488,8 @@ class MainActivity : BaseActivity(), VolumeAdapter.Listener {
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
shouldCloseVolume = true
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
volumeOpener.wipeSensitive()
|
||||
if (explorerRouter.pickMode && !usfKeepOpen && shouldCloseVolume) {
|
||||
IntentUtils.getParcelableExtra<EncryptedVolume>(intent, "volume")?.close()
|
||||
RestrictedFileProvider.wipeAll(this)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,17 @@ class VolumeData(val name: String, val isHidden: Boolean = false, val type: Byte
|
||||
}
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other !is VolumeData) {
|
||||
return false
|
||||
}
|
||||
return other.name == name && other.isHidden == isHidden
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return name.hashCode()+isHidden.hashCode()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VOLUMES_DIRECTORY = "volumes"
|
||||
|
||||
|
42
app/src/main/java/sushi/hardcore/droidfs/VolumeManager.kt
Normal file
42
app/src/main/java/sushi/hardcore/droidfs/VolumeManager.kt
Normal file
@ -0,0 +1,42 @@
|
||||
package sushi.hardcore.droidfs
|
||||
|
||||
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
||||
|
||||
class VolumeManager {
|
||||
private var id = 0
|
||||
private val volumes = HashMap<Int, EncryptedVolume>()
|
||||
private val volumesData = HashMap<VolumeData, Int>()
|
||||
|
||||
fun insert(volume: EncryptedVolume, data: VolumeData): Int {
|
||||
volumes[id] = volume
|
||||
volumesData[data] = id
|
||||
return id++
|
||||
}
|
||||
|
||||
fun isOpen(volume: VolumeData): Boolean {
|
||||
return volumesData.containsKey(volume)
|
||||
}
|
||||
|
||||
fun getVolumeId(volume: VolumeData): Int? {
|
||||
return volumesData[volume]
|
||||
}
|
||||
|
||||
fun getVolume(id: Int): EncryptedVolume? {
|
||||
return volumes[id]
|
||||
}
|
||||
|
||||
fun closeVolume(id: Int) {
|
||||
volumes.remove(id)?.let { volume ->
|
||||
volume.close()
|
||||
volumesData.filter { it.value == id }.forEach {
|
||||
volumesData.remove(it.key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun closeAll() {
|
||||
volumes.forEach { it.value.close() }
|
||||
volumes.clear()
|
||||
volumesData.clear()
|
||||
}
|
||||
}
|
49
app/src/main/java/sushi/hardcore/droidfs/VolumeManagerApp.kt
Normal file
49
app/src/main/java/sushi/hardcore/droidfs/VolumeManagerApp.kt
Normal file
@ -0,0 +1,49 @@
|
||||
package sushi.hardcore.droidfs
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ProcessLifecycleOwner
|
||||
import androidx.preference.PreferenceManager
|
||||
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
||||
|
||||
class VolumeManagerApp : Application(), DefaultLifecycleObserver {
|
||||
companion object {
|
||||
private const val USF_KEEP_OPEN_KEY = "usf_keep_open"
|
||||
}
|
||||
|
||||
lateinit var sharedPreferences: SharedPreferences
|
||||
private val sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == USF_KEEP_OPEN_KEY) {
|
||||
reloadUsfKeepOpen()
|
||||
}
|
||||
}
|
||||
private var usfKeepOpen = false
|
||||
var isStartingExternalApp = false
|
||||
val volumeManager = VolumeManager()
|
||||
|
||||
override fun onCreate() {
|
||||
super<Application>.onCreate()
|
||||
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this).apply {
|
||||
registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
|
||||
}
|
||||
reloadUsfKeepOpen()
|
||||
}
|
||||
|
||||
private fun reloadUsfKeepOpen() {
|
||||
usfKeepOpen = sharedPreferences.getBoolean(USF_KEEP_OPEN_KEY, false)
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) {
|
||||
isStartingExternalApp = false
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
if (!isStartingExternalApp && !usfKeepOpen) {
|
||||
volumeManager.closeAll()
|
||||
RestrictedFileProvider.wipeAll(applicationContext)
|
||||
}
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ class VolumeOpener(
|
||||
) {
|
||||
interface VolumeOpenerCallbacks {
|
||||
fun onHashStorageReset() {}
|
||||
fun onVolumeOpened(encryptedVolume: EncryptedVolume, volumeShortName: String)
|
||||
fun onVolumeOpened(id: Int)
|
||||
}
|
||||
|
||||
private val volumeDatabase = VolumeDatabase(activity)
|
||||
@ -31,6 +31,7 @@ class VolumeOpener(
|
||||
var themeValue = sharedPrefs.getString(Constants.THEME_VALUE_KEY, Constants.DEFAULT_THEME_VALUE)!!
|
||||
var defaultVolumeName: String? = sharedPrefs.getString(DEFAULT_VOLUME_KEY, null)
|
||||
private var dialogBinding: DialogOpenVolumeBinding? = null
|
||||
private val volumeManager = (activity.application as VolumeManagerApp).volumeManager
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
@ -40,54 +41,59 @@ class VolumeOpener(
|
||||
|
||||
@SuppressLint("NewApi") // fingerprintProtector is non-null only when SDK_INT >= 23
|
||||
fun openVolume(volume: VolumeData, isVolumeSaved: Boolean, callbacks: VolumeOpenerCallbacks) {
|
||||
if (volume.type == EncryptedVolume.GOCRYPTFS_VOLUME_TYPE && BuildConfig.GOCRYPTFS_DISABLED) {
|
||||
Toast.makeText(activity, R.string.gocryptfs_disabled, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
} else if (volume.type == EncryptedVolume.CRYFS_VOLUME_TYPE && BuildConfig.CRYFS_DISABLED) {
|
||||
Toast.makeText(activity, R.string.cryfs_disabled, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
var askForPassword = true
|
||||
fingerprintProtector?.let { fingerprintProtector ->
|
||||
volume.encryptedHash?.let { encryptedHash ->
|
||||
volume.iv?.let { iv ->
|
||||
askForPassword = false
|
||||
fingerprintProtector.listener = object : FingerprintProtector.Listener {
|
||||
override fun onHashStorageReset() {
|
||||
callbacks.onHashStorageReset()
|
||||
}
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {
|
||||
object : LoadingTask<EncryptedVolume?>(activity, themeValue, R.string.loading_msg_open) {
|
||||
override suspend fun doTask(): EncryptedVolume? {
|
||||
val encryptedVolume = EncryptedVolume.init(volume, activity.filesDir.path, null, hash, null)
|
||||
Arrays.fill(hash, 0)
|
||||
return encryptedVolume
|
||||
val volumeId = volumeManager.getVolumeId(volume)
|
||||
if (volumeId == null) {
|
||||
if (volume.type == EncryptedVolume.GOCRYPTFS_VOLUME_TYPE && BuildConfig.GOCRYPTFS_DISABLED) {
|
||||
Toast.makeText(activity, R.string.gocryptfs_disabled, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
} else if (volume.type == EncryptedVolume.CRYFS_VOLUME_TYPE && BuildConfig.CRYFS_DISABLED) {
|
||||
Toast.makeText(activity, R.string.cryfs_disabled, Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
var askForPassword = true
|
||||
fingerprintProtector?.let { fingerprintProtector ->
|
||||
volume.encryptedHash?.let { encryptedHash ->
|
||||
volume.iv?.let { iv ->
|
||||
askForPassword = false
|
||||
fingerprintProtector.listener = object : FingerprintProtector.Listener {
|
||||
override fun onHashStorageReset() {
|
||||
callbacks.onHashStorageReset()
|
||||
}
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {
|
||||
object : LoadingTask<EncryptedVolume?>(activity, themeValue, R.string.loading_msg_open) {
|
||||
override suspend fun doTask(): EncryptedVolume? {
|
||||
val encryptedVolume = EncryptedVolume.init(volume, activity.filesDir.path, null, hash, null)
|
||||
Arrays.fill(hash, 0)
|
||||
return encryptedVolume
|
||||
}
|
||||
}.startTask(activity.lifecycleScope) { encryptedVolume ->
|
||||
if (encryptedVolume == null) {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.open_volume_failed)
|
||||
.setMessage(R.string.open_failed_hash_msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
} else {
|
||||
callbacks.onVolumeOpened(volumeManager.insert(encryptedVolume, volume))
|
||||
}
|
||||
}
|
||||
}.startTask(activity.lifecycleScope) { encryptedVolume ->
|
||||
if (encryptedVolume == null) {
|
||||
CustomAlertDialogBuilder(activity, themeValue)
|
||||
.setTitle(R.string.open_volume_failed)
|
||||
.setMessage(R.string.open_failed_hash_msg)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
} else {
|
||||
callbacks.onVolumeOpened(encryptedVolume, volume.shortName)
|
||||
}
|
||||
override fun onPasswordHashSaved() {}
|
||||
override fun onFailed(pending: Boolean) {
|
||||
if (!pending) {
|
||||
askForPassword(volume, isVolumeSaved, callbacks)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onPasswordHashSaved() {}
|
||||
override fun onFailed(pending: Boolean) {
|
||||
if (!pending) {
|
||||
askForPassword(volume, isVolumeSaved, callbacks)
|
||||
}
|
||||
}
|
||||
fingerprintProtector.loadPasswordHash(volume.shortName, encryptedHash, iv)
|
||||
}
|
||||
fingerprintProtector.loadPasswordHash(volume.shortName, encryptedHash, iv)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (askForPassword) {
|
||||
askForPassword(volume, isVolumeSaved, callbacks)
|
||||
if (askForPassword) {
|
||||
askForPassword(volume, isVolumeSaved, callbacks)
|
||||
}
|
||||
} else {
|
||||
callbacks.onVolumeOpened(volumeId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,7 +194,7 @@ class VolumeOpener(
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {}
|
||||
override fun onPasswordHashSaved() {
|
||||
Arrays.fill(returnedHash.value!!, 0)
|
||||
callbacks.onVolumeOpened(encryptedVolume, volume.shortName)
|
||||
callbacks.onVolumeOpened(volumeManager.insert(encryptedVolume, volume))
|
||||
}
|
||||
private var isClosed = false
|
||||
override fun onFailed(pending: Boolean) {
|
||||
@ -201,7 +207,7 @@ class VolumeOpener(
|
||||
}
|
||||
fingerprintProtector.savePasswordHash(volume, returnedHash.value!!)
|
||||
} else {
|
||||
callbacks.onVolumeOpened(encryptedVolume, volume.shortName)
|
||||
callbacks.onVolumeOpened(volumeManager.insert(encryptedVolume, volume))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,15 +8,18 @@ import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import sushi.hardcore.droidfs.R
|
||||
import sushi.hardcore.droidfs.VolumeData
|
||||
import sushi.hardcore.droidfs.VolumeDatabase
|
||||
import sushi.hardcore.droidfs.VolumeManager
|
||||
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
||||
|
||||
class VolumeAdapter(
|
||||
private val context: Context,
|
||||
private val volumeDatabase: VolumeDatabase,
|
||||
private val volumeManager: VolumeManager,
|
||||
private val allowSelection: Boolean,
|
||||
private val showReadOnly: Boolean,
|
||||
private val listener: Listener,
|
||||
@ -80,16 +83,12 @@ class VolumeAdapter(
|
||||
fun bind(position: Int) {
|
||||
val volume = volumes[position]
|
||||
itemView.findViewById<TextView>(R.id.text_volume_name).text = volume.shortName
|
||||
itemView.findViewById<ImageView>(R.id.image_icon).setImageResource(R.drawable.icon_volume)
|
||||
itemView.findViewById<TextView>(R.id.text_path).text = if (volume.isHidden)
|
||||
context.getString(R.string.hidden_volume)
|
||||
else
|
||||
volume.name
|
||||
itemView.findViewById<ImageView>(R.id.icon_fingerprint).visibility = if (volume.encryptedHash == null) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
itemView.findViewById<ImageView>(R.id.icon_unlocked).isVisible = volumeManager.isOpen(volume)
|
||||
itemView.findViewById<ImageView>(R.id.icon_fingerprint).isVisible = volume.encryptedHash != null
|
||||
itemView.findViewById<TextView>(R.id.text_info).text = context.getString(
|
||||
if (volume.canWrite(context.filesDir.path)) {
|
||||
R.string.volume_type
|
||||
|
@ -4,11 +4,8 @@ import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.addCallback
|
||||
import sushi.hardcore.droidfs.*
|
||||
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
||||
import sushi.hardcore.droidfs.databinding.ActivityAddVolumeBinding
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerRouter
|
||||
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
||||
import sushi.hardcore.droidfs.util.IntentUtils
|
||||
|
||||
class AddVolumeActivity: BaseActivity() {
|
||||
|
||||
@ -19,15 +16,12 @@ class AddVolumeActivity: BaseActivity() {
|
||||
private lateinit var binding: ActivityAddVolumeBinding
|
||||
private lateinit var explorerRouter: ExplorerRouter
|
||||
private lateinit var volumeOpener: VolumeOpener
|
||||
private var usfKeepOpen = false
|
||||
var shouldCloseVolume = true // used when launched to pick file from another volume
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityAddVolumeBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
usfKeepOpen = sharedPrefs.getBoolean("usf_keep_open", false)
|
||||
explorerRouter = ExplorerRouter(this, intent)
|
||||
volumeOpener = VolumeOpener(this)
|
||||
if (savedInstanceState == null) {
|
||||
@ -41,7 +35,6 @@ class AddVolumeActivity: BaseActivity() {
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
setResult(RESULT_USER_BACK)
|
||||
shouldCloseVolume = false
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
@ -53,7 +46,6 @@ class AddVolumeActivity: BaseActivity() {
|
||||
supportFragmentManager.popBackStack()
|
||||
else {
|
||||
setResult(RESULT_USER_BACK)
|
||||
shouldCloseVolume = false
|
||||
finish()
|
||||
}
|
||||
}
|
||||
@ -70,21 +62,19 @@ class AddVolumeActivity: BaseActivity() {
|
||||
)
|
||||
}
|
||||
|
||||
fun startExplorer(encryptedVolume: EncryptedVolume, volumeShortName: String) {
|
||||
startActivity(explorerRouter.getExplorerIntent(encryptedVolume, volumeShortName))
|
||||
shouldCloseVolume = false
|
||||
fun startExplorer(volumeId: Int, volumeShortName: String) {
|
||||
startActivity(explorerRouter.getExplorerIntent(volumeId, volumeShortName))
|
||||
finish()
|
||||
}
|
||||
|
||||
fun onVolumeSelected(volume: VolumeData, rememberVolume: Boolean) {
|
||||
if (rememberVolume) {
|
||||
setResult(RESULT_USER_BACK)
|
||||
shouldCloseVolume = false
|
||||
finish()
|
||||
} else {
|
||||
volumeOpener.openVolume(volume, false, object : VolumeOpener.VolumeOpenerCallbacks {
|
||||
override fun onVolumeOpened(encryptedVolume: EncryptedVolume, volumeShortName: String) {
|
||||
startExplorer(encryptedVolume, volumeShortName)
|
||||
override fun onVolumeOpened(id: Int) {
|
||||
startExplorer(id, volume.shortName)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -106,18 +96,4 @@ class AddVolumeActivity: BaseActivity() {
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
shouldCloseVolume = true
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if (explorerRouter.pickMode && !usfKeepOpen && shouldCloseVolume) {
|
||||
IntentUtils.getParcelableExtra<EncryptedVolume>(intent, "volume")?.close()
|
||||
RestrictedFileProvider.wipeAll(this)
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
@ -158,11 +158,7 @@ class CreateVolumeFragment: Fragment() {
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val encryptedVolume = if (rememberVolume) {
|
||||
null
|
||||
} else {
|
||||
ObjRef<EncryptedVolume?>(null)
|
||||
}
|
||||
val encryptedVolume = ObjRef<EncryptedVolume?>(null)
|
||||
object: LoadingTask<Byte>(requireActivity() as AppCompatActivity, themeValue, R.string.loading_msg_create) {
|
||||
private fun generateResult(success: Boolean, volumeType: Byte): Byte {
|
||||
return if (success) {
|
||||
@ -223,6 +219,9 @@ class CreateVolumeFragment: Fragment() {
|
||||
isVolumeSaved = saveVolume(volume)
|
||||
}
|
||||
}
|
||||
val volumeId = encryptedVolume.value?.let {
|
||||
(activity?.application as VolumeManagerApp).volumeManager.insert(it, volume)
|
||||
}
|
||||
@SuppressLint("NewApi") // if fingerprintProtector is null checkboxSavePassword is hidden
|
||||
if (isVolumeSaved && binding.checkboxSavePassword.isChecked && returnedHash != null) {
|
||||
fingerprintProtector!!.let {
|
||||
@ -235,31 +234,31 @@ class CreateVolumeFragment: Fragment() {
|
||||
override fun onPasswordHashDecrypted(hash: ByteArray) {} // shouldn't happen here
|
||||
override fun onPasswordHashSaved() {
|
||||
Arrays.fill(returnedHash.value!!, 0)
|
||||
onVolumeCreated(encryptedVolume?.value, volume.shortName)
|
||||
onVolumeCreated(volumeId, volume.shortName)
|
||||
}
|
||||
override fun onFailed(pending: Boolean) {
|
||||
if (!pending) {
|
||||
Arrays.fill(returnedHash.value!!, 0)
|
||||
onVolumeCreated(encryptedVolume?.value, volume.shortName)
|
||||
onVolumeCreated(volumeId, volume.shortName)
|
||||
}
|
||||
}
|
||||
}
|
||||
it.savePasswordHash(volume, returnedHash.value!!)
|
||||
}
|
||||
} else {
|
||||
onVolumeCreated(encryptedVolume?.value, volume.shortName)
|
||||
onVolumeCreated(volumeId, volume.shortName)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onVolumeCreated(encryptedVolume: EncryptedVolume?, volumeShortName: String) {
|
||||
private fun onVolumeCreated(id: Int?, volumeShortName: String) {
|
||||
(activity as AddVolumeActivity).apply {
|
||||
if (rememberVolume || encryptedVolume == null) {
|
||||
if (rememberVolume || id == null) {
|
||||
finish()
|
||||
} else {
|
||||
startExplorer(encryptedVolume, volumeShortName)
|
||||
startExplorer(id, volumeShortName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import sushi.hardcore.droidfs.Constants
|
||||
import sushi.hardcore.droidfs.R
|
||||
import sushi.hardcore.droidfs.VolumeData
|
||||
import sushi.hardcore.droidfs.VolumeDatabase
|
||||
import sushi.hardcore.droidfs.VolumeManagerApp
|
||||
import sushi.hardcore.droidfs.databinding.DialogSdcardErrorBinding
|
||||
import sushi.hardcore.droidfs.databinding.FragmentSelectPathBinding
|
||||
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
||||
@ -62,6 +63,7 @@ class SelectPathFragment: Fragment() {
|
||||
if (uri != null)
|
||||
onDirectoryPicked(uri)
|
||||
}
|
||||
private lateinit var app: VolumeManagerApp
|
||||
private var themeValue = Constants.DEFAULT_THEME_VALUE
|
||||
private lateinit var volumeDatabase: VolumeDatabase
|
||||
private lateinit var filesDir: String
|
||||
@ -81,6 +83,7 @@ class SelectPathFragment: Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
app = requireActivity().application as VolumeManagerApp
|
||||
sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
originalRememberVolume = sharedPrefs.getBoolean(Constants.REMEMBER_VOLUME_KEY, true)
|
||||
binding.switchRemember.isChecked = originalRememberVolume
|
||||
@ -105,6 +108,7 @@ class SelectPathFragment: Fragment() {
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
launchPickDirectory()
|
||||
} else {
|
||||
app.isStartingExternalApp = true
|
||||
startActivity(Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Uri.parse("package:"+requireContext().packageName)))
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
@ -119,6 +123,7 @@ class SelectPathFragment: Fragment() {
|
||||
) {
|
||||
launchPickDirectory()
|
||||
} else {
|
||||
app.isStartingExternalApp = true
|
||||
askStoragePermissions.launch(
|
||||
arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
@ -149,7 +154,7 @@ class SelectPathFragment: Fragment() {
|
||||
}
|
||||
|
||||
private fun launchPickDirectory() {
|
||||
(activity as AddVolumeActivity).shouldCloseVolume = false
|
||||
app.isStartingExternalApp = true
|
||||
PathUtils.safePickDirectory(pickDirectory, requireContext(), themeValue)
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ import sushi.hardcore.droidfs.util.SQLUtil.appendSelectionArgs
|
||||
import sushi.hardcore.droidfs.util.SQLUtil.concatenateWhere
|
||||
import sushi.hardcore.droidfs.util.Wiper
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
import java.util.UUID
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class RestrictedFileProvider: ContentProvider() {
|
||||
|
@ -13,6 +13,7 @@ import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.addCallback
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
@ -22,20 +23,15 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import kotlinx.coroutines.*
|
||||
import sushi.hardcore.droidfs.BaseActivity
|
||||
import sushi.hardcore.droidfs.Constants
|
||||
import sushi.hardcore.droidfs.FileTypes
|
||||
import sushi.hardcore.droidfs.R
|
||||
import sushi.hardcore.droidfs.*
|
||||
import sushi.hardcore.droidfs.adapters.ExplorerElementAdapter
|
||||
import sushi.hardcore.droidfs.adapters.OpenAsDialogAdapter
|
||||
import sushi.hardcore.droidfs.content_providers.ExternalProvider
|
||||
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
||||
import sushi.hardcore.droidfs.file_operations.FileOperationService
|
||||
import sushi.hardcore.droidfs.file_operations.OperationFile
|
||||
import sushi.hardcore.droidfs.file_viewers.*
|
||||
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
||||
import sushi.hardcore.droidfs.filesystems.Stat
|
||||
import sushi.hardcore.droidfs.util.IntentUtils
|
||||
import sushi.hardcore.droidfs.util.PathUtils
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import sushi.hardcore.droidfs.widgets.EditTextDialog
|
||||
@ -46,6 +42,7 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
||||
private var foldersFirst = true
|
||||
private var mapFolders = true
|
||||
private var currentSortOrderIndex = 0
|
||||
private var volumeId = -1
|
||||
protected lateinit var encryptedVolume: EncryptedVolume
|
||||
private lateinit var volumeName: String
|
||||
private lateinit var explorerViewModel: ExplorerViewModel
|
||||
@ -58,10 +55,8 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
||||
protected val taskScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
protected lateinit var explorerElements: MutableList<ExplorerElement>
|
||||
protected lateinit var explorerAdapter: ExplorerElementAdapter
|
||||
private var isCreating = true
|
||||
protected var isStartingActivity = false
|
||||
protected lateinit var app: VolumeManagerApp
|
||||
private var usf_open = false
|
||||
protected var usf_keep_open = false
|
||||
private lateinit var linearLayoutManager: LinearLayoutManager
|
||||
private var isUsingListLayout = true
|
||||
private lateinit var layoutIcon: ImageButton
|
||||
@ -76,10 +71,11 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
app = application as VolumeManagerApp
|
||||
usf_open = sharedPrefs.getBoolean("usf_open", false)
|
||||
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
|
||||
volumeName = intent.getStringExtra("volume_name") ?: ""
|
||||
encryptedVolume = IntentUtils.getParcelableExtra(intent, "volume")!!
|
||||
volumeName = intent.getStringExtra("volumeName") ?: ""
|
||||
volumeId = intent.getIntExtra("volumeId", -1)
|
||||
encryptedVolume = app.volumeManager.getVolume(volumeId)!!
|
||||
sortOrderEntries = resources.getStringArray(R.array.sort_orders_entries)
|
||||
sortOrderValues = resources.getStringArray(R.array.sort_orders_values)
|
||||
foldersFirst = sharedPrefs.getBoolean("folders_first", true)
|
||||
@ -118,6 +114,19 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
||||
isUsingListLayout = sharedPrefs.getBoolean("useListLayout", true)
|
||||
layoutIcon = findViewById(R.id.layout_icon)
|
||||
setRecyclerViewLayout()
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
if (explorerAdapter.selectedItems.isEmpty()) {
|
||||
val parentPath = PathUtils.getParentPath(currentDirectoryPath)
|
||||
if (parentPath == currentDirectoryPath) {
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
} else {
|
||||
setCurrentPath(PathUtils.getParentPath(currentDirectoryPath))
|
||||
}
|
||||
} else {
|
||||
unselectAll()
|
||||
}
|
||||
}
|
||||
layoutIcon.setOnClickListener {
|
||||
isUsingListLayout = !isUsingListLayout
|
||||
setRecyclerViewLayout()
|
||||
@ -171,18 +180,17 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
||||
}
|
||||
}
|
||||
|
||||
private fun startFileViewer(cls: Class<*>, filePath: String){
|
||||
private fun startFileViewer(cls: Class<*>, filePath: String) {
|
||||
val intent = Intent(this, cls).apply {
|
||||
putExtra("path", filePath)
|
||||
putExtra("volume", encryptedVolume)
|
||||
putExtra("sortOrder", sortOrderValues[currentSortOrderIndex])
|
||||
}
|
||||
isStartingActivity = true
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun openWithExternalApp(fullPath: String){
|
||||
isStartingActivity = true
|
||||
private fun openWithExternalApp(fullPath: String) {
|
||||
app.isStartingExternalApp = true
|
||||
ExternalProvider.open(this, themeValue, encryptedVolume, fullPath)
|
||||
}
|
||||
|
||||
@ -332,28 +340,18 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
||||
}
|
||||
}
|
||||
|
||||
private fun askCloseVolume() {
|
||||
private fun askLockVolume() {
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.warning)
|
||||
.setMessage(R.string.ask_close_volume)
|
||||
.setPositiveButton(R.string.ok) { _, _ -> closeVolumeOnUserExit() }
|
||||
.setMessage(R.string.ask_lock_volume)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
app.volumeManager.closeVolume(volumeId)
|
||||
finish()
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (explorerAdapter.selectedItems.isEmpty()) {
|
||||
val parentPath = PathUtils.getParentPath(currentDirectoryPath)
|
||||
if (parentPath == currentDirectoryPath) {
|
||||
askCloseVolume()
|
||||
} else {
|
||||
setCurrentPath(PathUtils.getParentPath(currentDirectoryPath))
|
||||
}
|
||||
} else {
|
||||
unselectAll()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFolder(folderName: String){
|
||||
if (folderName.isEmpty()) {
|
||||
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
|
||||
@ -508,9 +506,9 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
||||
val noItemSelected = explorerAdapter.selectedItems.isEmpty()
|
||||
val iconColor = ContextCompat.getColor(this, R.color.neutralIconTint)
|
||||
setMenuIconTint(menu, iconColor, R.id.sort, R.drawable.icon_sort)
|
||||
setMenuIconTint(menu, iconColor, R.id.decrypt, R.drawable.icon_decrypt)
|
||||
setMenuIconTint(menu, iconColor, R.id.share, R.drawable.icon_share)
|
||||
menu.findItem(R.id.sort).isVisible = noItemSelected
|
||||
menu.findItem(R.id.lock).isVisible = noItemSelected
|
||||
menu.findItem(R.id.close).isVisible = noItemSelected
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(!noItemSelected)
|
||||
if (!noItemSelected) {
|
||||
@ -577,55 +575,33 @@ open class BaseExplorerActivity : BaseActivity(), ExplorerElementAdapter.Listene
|
||||
true
|
||||
}
|
||||
R.id.close -> {
|
||||
askCloseVolume()
|
||||
finish()
|
||||
true
|
||||
}
|
||||
R.id.lock -> {
|
||||
askLockVolume()
|
||||
true
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun closeVolumeOnUserExit() {
|
||||
finish()
|
||||
}
|
||||
|
||||
protected open fun closeVolumeOnDestroy() {
|
||||
taskScope.cancel()
|
||||
if (!encryptedVolume.isClosed()) {
|
||||
encryptedVolume.close()
|
||||
}
|
||||
RestrictedFileProvider.wipeAll(this) //additional security
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (!isChangingConfigurations) { //activity won't be recreated
|
||||
closeVolumeOnDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (!isChangingConfigurations){
|
||||
if (isStartingActivity){
|
||||
isStartingActivity = false
|
||||
} else if (!usf_keep_open){
|
||||
finish()
|
||||
}
|
||||
taskScope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (isCreating){
|
||||
isCreating = false
|
||||
if (app.isStartingExternalApp) {
|
||||
ExternalProvider.removeFilesAsync(this)
|
||||
}
|
||||
if (encryptedVolume.isClosed()) {
|
||||
finish()
|
||||
} else {
|
||||
if (encryptedVolume.isClosed()) {
|
||||
finish()
|
||||
} else {
|
||||
isStartingActivity = false
|
||||
ExternalProvider.removeFilesAsync(this)
|
||||
setCurrentPath(currentDirectoryPath)
|
||||
}
|
||||
setCurrentPath(currentDirectoryPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import android.net.Uri
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
@ -60,9 +61,7 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
}
|
||||
if (operationFiles.size > 0){
|
||||
checkPathOverwrite(operationFiles, currentDirectoryPath) { items ->
|
||||
if (items == null) {
|
||||
remoteEncryptedVolume.close()
|
||||
} else {
|
||||
if (items != null) {
|
||||
// stop loading thumbnails while writing files
|
||||
explorerAdapter.loadThumbnails = false
|
||||
taskScope.launch {
|
||||
@ -78,12 +77,9 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
}
|
||||
explorerAdapter.loadThumbnails = true
|
||||
setCurrentPath(currentDirectoryPath)
|
||||
remoteEncryptedVolume.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
remoteEncryptedVolume.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -169,6 +165,16 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
|
||||
override fun init() {
|
||||
super.init()
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
if (currentItemAction != ItemsActions.NONE) {
|
||||
cancelItemAction()
|
||||
invalidateOptionsMenu()
|
||||
} else {
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
isEnabled = true
|
||||
}
|
||||
}
|
||||
findViewById<FloatingActionButton>(R.id.fab).setOnClickListener {
|
||||
if (currentItemAction != ItemsActions.NONE){
|
||||
openDialogCreateFolder()
|
||||
@ -189,15 +195,14 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.action = "pick"
|
||||
intent.putExtra("volume", encryptedVolume)
|
||||
isStartingActivity = true
|
||||
pickFromOtherVolumes.launch(intent)
|
||||
}
|
||||
"importFiles" -> {
|
||||
isStartingActivity = true
|
||||
app.isStartingExternalApp = true
|
||||
pickFiles.launch(arrayOf("*/*"))
|
||||
}
|
||||
"importFolder" -> {
|
||||
isStartingActivity = true
|
||||
app.isStartingExternalApp = true
|
||||
pickImportDirectory.launch(null)
|
||||
}
|
||||
"createFile" -> {
|
||||
@ -212,7 +217,6 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
val intent = Intent(this, CameraActivity::class.java)
|
||||
intent.putExtra("path", currentDirectoryPath)
|
||||
intent.putExtra("volume", encryptedVolume)
|
||||
isStartingActivity = true
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
@ -257,6 +261,7 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
val result = super.onCreateOptionsMenu(menu)
|
||||
if (currentItemAction != ItemsActions.NONE) {
|
||||
menu.findItem(R.id.validate).isVisible = true
|
||||
menu.findItem(R.id.lock).isVisible = false
|
||||
menu.findItem(R.id.close).isVisible = false
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
} else {
|
||||
@ -405,13 +410,13 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
for (i in explorerAdapter.selectedItems) {
|
||||
paths.add(explorerElements[i].fullPath)
|
||||
}
|
||||
isStartingActivity = true
|
||||
app.isStartingExternalApp = true
|
||||
ExternalProvider.share(this, themeValue, encryptedVolume, paths)
|
||||
unselectAll()
|
||||
true
|
||||
}
|
||||
R.id.decrypt -> {
|
||||
isStartingActivity = true
|
||||
app.isStartingExternalApp = true
|
||||
pickExportDirectory.launch(null)
|
||||
true
|
||||
}
|
||||
@ -506,13 +511,4 @@ class ExplorerActivity : BaseExplorerActivity() {
|
||||
itemsToProcess.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (currentItemAction != ItemsActions.NONE) {
|
||||
cancelItemAction()
|
||||
invalidateOptionsMenu()
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,21 +22,18 @@ class ExplorerActivityPick : BaseExplorerActivity() {
|
||||
}
|
||||
|
||||
override fun onExplorerElementClick(position: Int) {
|
||||
val wasSelecting = explorerAdapter.selectedItems.isNotEmpty()
|
||||
if (explorerAdapter.selectedItems.isEmpty()) {
|
||||
if (!wasSelecting) {
|
||||
val fullPath = PathUtils.pathJoin(currentDirectoryPath, explorerElements[position].name)
|
||||
when {
|
||||
explorerElements[position].isDirectory -> {
|
||||
setCurrentPath(fullPath)
|
||||
}
|
||||
explorerElements[position].isParentFolder -> {
|
||||
setCurrentPath(PathUtils.getParentPath(currentDirectoryPath))
|
||||
}
|
||||
else -> {
|
||||
resultIntent.putExtra("path", fullPath)
|
||||
returnActivityResult()
|
||||
}
|
||||
val fullPath = PathUtils.pathJoin(currentDirectoryPath, explorerElements[position].name)
|
||||
when {
|
||||
explorerElements[position].isDirectory -> {
|
||||
setCurrentPath(fullPath)
|
||||
}
|
||||
explorerElements[position].isParentFolder -> {
|
||||
setCurrentPath(PathUtils.getParentPath(currentDirectoryPath))
|
||||
}
|
||||
else -> {
|
||||
resultIntent.putExtra("path", fullPath)
|
||||
returnActivityResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -81,17 +78,4 @@ class ExplorerActivityPick : BaseExplorerActivity() {
|
||||
isFinishingIntentionally = true
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun closeVolumeOnDestroy() {
|
||||
if (!isFinishingIntentionally && !usf_keep_open){
|
||||
IntentUtils.getParcelableExtra<EncryptedVolume>(intent, "destinationVolume")?.close()
|
||||
super.closeVolumeOnDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
override fun closeVolumeOnUserExit() {
|
||||
isFinishingIntentionally = true
|
||||
super.closeVolumeOnUserExit()
|
||||
super.closeVolumeOnDestroy()
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ class ExplorerRouter(private val context: Context, private val intent: Intent) {
|
||||
var pickMode = intent.action == "pick"
|
||||
var dropMode = (intent.action == Intent.ACTION_SEND || intent.action == Intent.ACTION_SEND_MULTIPLE) && intent.extras != null
|
||||
|
||||
fun getExplorerIntent(encryptedVolume: EncryptedVolume, volumeShortName: String): Intent {
|
||||
fun getExplorerIntent(volumeId: Int, volumeShortName: String): Intent {
|
||||
var explorerIntent: Intent? = null
|
||||
if (dropMode) { //import via android share menu
|
||||
explorerIntent = Intent(context, ExplorerActivityDrop::class.java)
|
||||
@ -22,8 +22,8 @@ class ExplorerRouter(private val context: Context, private val intent: Intent) {
|
||||
if (explorerIntent == null) {
|
||||
explorerIntent = Intent(context, ExplorerActivity::class.java) //default opening
|
||||
}
|
||||
explorerIntent.putExtra("volume", encryptedVolume)
|
||||
explorerIntent.putExtra("volume_name", volumeShortName)
|
||||
explorerIntent.putExtra("volumeId", volumeId)
|
||||
explorerIntent.putExtra("volumeName", volumeShortName)
|
||||
return explorerIntent
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ package sushi.hardcore.droidfs.file_viewers
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.activity.addCallback
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
@ -14,7 +13,6 @@ import kotlinx.coroutines.withContext
|
||||
import sushi.hardcore.droidfs.BaseActivity
|
||||
import sushi.hardcore.droidfs.FileTypes
|
||||
import sushi.hardcore.droidfs.R
|
||||
import sushi.hardcore.droidfs.content_providers.RestrictedFileProvider
|
||||
import sushi.hardcore.droidfs.explorers.ExplorerElement
|
||||
import sushi.hardcore.droidfs.filesystems.EncryptedVolume
|
||||
import sushi.hardcore.droidfs.util.IntentUtils
|
||||
@ -27,8 +25,6 @@ abstract class FileViewerActivity: BaseActivity() {
|
||||
private lateinit var originalParentPath: String
|
||||
private lateinit var windowInsetsController: WindowInsetsControllerCompat
|
||||
private var windowTypeMask = 0
|
||||
private var isFinishingIntentionally = false
|
||||
private var usf_keep_open = false
|
||||
private var foldersFirst = true
|
||||
private var wasMapped = false
|
||||
protected val mappedPlaylist = mutableListOf<ExplorerElement>()
|
||||
@ -43,17 +39,11 @@ abstract class FileViewerActivity: BaseActivity() {
|
||||
filePath = intent.getStringExtra("path")!!
|
||||
originalParentPath = PathUtils.getParentPath(filePath)
|
||||
encryptedVolume = IntentUtils.getParcelableExtra(intent, "volume")!!
|
||||
usf_keep_open = sharedPrefs.getBoolean("usf_keep_open", false)
|
||||
foldersFirst = sharedPrefs.getBoolean("folders_first", true)
|
||||
windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
|
||||
windowInsetsController.addOnControllableInsetsChangedListener { _, typeMask ->
|
||||
windowTypeMask = typeMask
|
||||
}
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
isFinishingIntentionally = true
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
if (fullscreenMode) {
|
||||
fixNavBarColor()
|
||||
hideSystemUi()
|
||||
@ -156,21 +146,12 @@ abstract class FileViewerActivity: BaseActivity() {
|
||||
}
|
||||
|
||||
protected fun goBackToExplorer() {
|
||||
isFinishingIntentionally = true
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (!isFinishingIntentionally) {
|
||||
encryptedVolume.close()
|
||||
RestrictedFileProvider.wipeAll(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (!usf_keep_open) {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (encryptedVolume.isClosed()) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.activity.addCallback
|
||||
import sushi.hardcore.droidfs.R
|
||||
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
|
||||
import java.io.File
|
||||
@ -29,6 +30,9 @@ class TextEditor: FileViewerActivity() {
|
||||
loadWholeFile(filePath) {
|
||||
try {
|
||||
loadLayout(String(it))
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
checkSaveAndExit()
|
||||
}
|
||||
} catch (e: OutOfMemoryError){
|
||||
CustomAlertDialogBuilder(this, themeValue)
|
||||
.setTitle(R.string.error)
|
||||
@ -124,8 +128,4 @@ class TextEditor: FileViewerActivity() {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
checkSaveAndExit()
|
||||
}
|
||||
}
|
||||
|
@ -67,13 +67,9 @@ class CryfsVolume(private val fusePtr: Long): EncryptedVolume() {
|
||||
}
|
||||
}
|
||||
|
||||
fun create(baseDir: String, localStateDir: String, password: ByteArray, returnedHash: ObjRef<ByteArray?>?, cipher: String?, volume: ObjRef<EncryptedVolume?>?): Boolean {
|
||||
fun create(baseDir: String, localStateDir: String, password: ByteArray, returnedHash: ObjRef<ByteArray?>?, cipher: String?, volume: ObjRef<EncryptedVolume?>): Boolean {
|
||||
return init(baseDir, localStateDir, password, null, returnedHash, true, cipher)?.also {
|
||||
if (volume == null) {
|
||||
it.close()
|
||||
} else {
|
||||
volume.value = it
|
||||
}
|
||||
volume.value = it
|
||||
} != null
|
||||
}
|
||||
|
||||
|
@ -36,7 +36,6 @@ class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
|
||||
logN: Int,
|
||||
creator: String,
|
||||
returnedHash: ByteArray?,
|
||||
openAfterCreation: Boolean,
|
||||
): Int
|
||||
private external fun nativeInit(root_cipher_dir: String, password: ByteArray?, givenHash: ByteArray?, returnedHash: ByteArray?): Int
|
||||
external fun changePassword(
|
||||
@ -53,20 +52,26 @@ class GocryptfsVolume(private val sessionID: Int): EncryptedVolume() {
|
||||
plainTextNames: Boolean,
|
||||
xchacha: Int,
|
||||
returnedHash: ByteArray?,
|
||||
volume: ObjRef<EncryptedVolume?>?
|
||||
volume: ObjRef<EncryptedVolume?>
|
||||
): Boolean {
|
||||
val openAfterCreation = volume != null
|
||||
val result = nativeCreateVolume(root_cipher_dir, password, plainTextNames, xchacha, ScryptDefaultLogN, VOLUME_CREATOR, returnedHash, openAfterCreation)
|
||||
return if (!openAfterCreation) {
|
||||
result == 1
|
||||
} else if (result == -1) {
|
||||
Log.e("gocryptfs", "Failed to open volume after creation")
|
||||
true
|
||||
} else if (result == -2) {
|
||||
false
|
||||
} else {
|
||||
volume!!.value = GocryptfsVolume(result)
|
||||
true
|
||||
return when (val result = nativeCreateVolume(
|
||||
root_cipher_dir,
|
||||
password,
|
||||
plainTextNames,
|
||||
xchacha,
|
||||
ScryptDefaultLogN,
|
||||
VOLUME_CREATOR,
|
||||
returnedHash,
|
||||
)) {
|
||||
-1 -> {
|
||||
Log.e("gocryptfs", "Failed to open volume after creation")
|
||||
true
|
||||
}
|
||||
-2 -> false
|
||||
else -> {
|
||||
volume.value = GocryptfsVolume(result)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
#include <jni.h>
|
||||
#include <stdio.h>
|
||||
#include <malloc.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include "libgocryptfs.h"
|
||||
|
||||
const int KeyLen = 32;
|
||||
@ -15,38 +13,34 @@ Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_00024Companion_nativeCre
|
||||
jint xchacha,
|
||||
jint logN,
|
||||
jstring jcreator,
|
||||
jbyteArray jreturned_hash,
|
||||
jboolean open_after_creation) {
|
||||
jbyteArray jreturned_hash) {
|
||||
const char* root_cipher_dir = (*env)->GetStringUTFChars(env, jroot_cipher_dir, NULL);
|
||||
const char* creator = (*env)->GetStringUTFChars(env, jcreator, NULL);
|
||||
GoString gofilename = {root_cipher_dir, strlen(root_cipher_dir)}, gocreator = {creator, strlen(creator)};
|
||||
|
||||
const size_t password_len = (*env)->GetArrayLength(env, jpassword);
|
||||
const size_t password_len = (const size_t) (*env)->GetArrayLength(env, jpassword);
|
||||
jbyte* password = (*env)->GetByteArrayElements(env, jpassword, NULL);
|
||||
GoSlice go_password = {password, password_len, password_len};
|
||||
|
||||
size_t returned_hash_len;
|
||||
GoSlice go_returned_hash;
|
||||
if (!(*env)->IsSameObject(env, jreturned_hash, NULL)) {
|
||||
returned_hash_len = (*env)->GetArrayLength(env, jreturned_hash);
|
||||
returned_hash_len = (size_t) (*env)->GetArrayLength(env, jreturned_hash);
|
||||
go_returned_hash.data = (*env)->GetByteArrayElements(env, jreturned_hash, NULL);
|
||||
} else if (open_after_creation) {
|
||||
} else {
|
||||
returned_hash_len = KeyLen;
|
||||
go_returned_hash.data = malloc(KeyLen);
|
||||
} else {
|
||||
returned_hash_len = 0;
|
||||
go_returned_hash.data = NULL;
|
||||
}
|
||||
go_returned_hash.len = returned_hash_len;
|
||||
go_returned_hash.cap = returned_hash_len;
|
||||
|
||||
GoUint8 result = gcf_create_volume(gofilename, go_password, plainTextNames, xchacha, logN, gocreator, go_returned_hash);
|
||||
GoUint8 result = gcf_create_volume(gofilename, go_password, plainTextNames, (GoInt8) xchacha, logN, gocreator, go_returned_hash);
|
||||
|
||||
(*env)->ReleaseByteArrayElements(env, jpassword, password, 0);
|
||||
(*env)->ReleaseStringUTFChars(env, jcreator, creator);
|
||||
|
||||
GoInt sessionID = -2;
|
||||
if (result && open_after_creation) {
|
||||
if (result) {
|
||||
GoSlice null_slice = {NULL, 0, 0};
|
||||
sessionID = gcf_init(gofilename, null_slice, go_returned_hash, null_slice);
|
||||
}
|
||||
@ -55,14 +49,14 @@ Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_00024Companion_nativeCre
|
||||
|
||||
if (!(*env)->IsSameObject(env, jreturned_hash, NULL)) {
|
||||
(*env)->ReleaseByteArrayElements(env, jreturned_hash, go_returned_hash.data, 0);
|
||||
} else if (open_after_creation) {
|
||||
} else {
|
||||
for (unsigned int i=0; i<returned_hash_len; ++i) {
|
||||
((unsigned char*) go_returned_hash.data)[i] = 0;
|
||||
}
|
||||
free(go_returned_hash.data);
|
||||
}
|
||||
|
||||
return sessionID*open_after_creation+result*!open_after_creation;
|
||||
return sessionID;
|
||||
}
|
||||
|
||||
JNIEXPORT jint JNICALL
|
||||
@ -81,13 +75,13 @@ Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_00024Companion_nativeIni
|
||||
jbyte* given_hash;
|
||||
GoSlice go_given_hash = {NULL, 0, 0};
|
||||
if ((*env)->IsSameObject(env, jgiven_hash, NULL)){
|
||||
password_len = (*env)->GetArrayLength(env, jpassword);
|
||||
password_len = (size_t) (*env)->GetArrayLength(env, jpassword);
|
||||
password = (*env)->GetByteArrayElements(env, jpassword, NULL);
|
||||
go_password.data = password;
|
||||
go_password.len = password_len;
|
||||
go_password.cap = password_len;
|
||||
} else {
|
||||
given_hash_len = (*env)->GetArrayLength(env, jgiven_hash);
|
||||
given_hash_len = (size_t) (*env)->GetArrayLength(env, jgiven_hash);
|
||||
given_hash = (*env)->GetByteArrayElements(env, jgiven_hash, NULL);
|
||||
go_given_hash.data = given_hash;
|
||||
go_given_hash.len = given_hash_len;
|
||||
@ -98,7 +92,7 @@ Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_00024Companion_nativeIni
|
||||
jbyte* returned_hash;
|
||||
GoSlice go_returned_hash = {NULL, 0, 0};
|
||||
if (!(*env)->IsSameObject(env, jreturned_hash, NULL)){
|
||||
returned_hash_len = (*env)->GetArrayLength(env, jreturned_hash);
|
||||
returned_hash_len = (size_t) (*env)->GetArrayLength(env, jreturned_hash);
|
||||
returned_hash = (*env)->GetByteArrayElements(env, jreturned_hash, NULL);
|
||||
go_returned_hash.data = returned_hash;
|
||||
go_returned_hash.len = returned_hash_len;
|
||||
@ -145,20 +139,20 @@ Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_00024Companion_changePas
|
||||
jbyte* given_hash;
|
||||
GoSlice go_given_hash = {NULL, 0, 0};
|
||||
if ((*env)->IsSameObject(env, jgiven_hash, NULL)){
|
||||
old_password_len = (*env)->GetArrayLength(env, jold_password);
|
||||
old_password_len = (size_t) (*env)->GetArrayLength(env, jold_password);
|
||||
old_password = (*env)->GetByteArrayElements(env, jold_password, NULL);
|
||||
go_old_password.data = old_password;
|
||||
go_old_password.len = old_password_len;
|
||||
go_old_password.cap = old_password_len;
|
||||
} else {
|
||||
given_hash_len = (*env)->GetArrayLength(env, jgiven_hash);
|
||||
given_hash_len = (size_t) (*env)->GetArrayLength(env, jgiven_hash);
|
||||
given_hash = (*env)->GetByteArrayElements(env, jgiven_hash, NULL);
|
||||
go_given_hash.data = given_hash;
|
||||
go_given_hash.len = given_hash_len;
|
||||
go_given_hash.cap = given_hash_len;
|
||||
}
|
||||
|
||||
size_t new_password_len = (*env)->GetArrayLength(env, jnew_password);
|
||||
size_t new_password_len = (size_t) (*env)->GetArrayLength(env, jnew_password);
|
||||
jbyte* new_password = (*env)->GetByteArrayElements(env, jnew_password, NULL);
|
||||
GoSlice go_new_password = {new_password, new_password_len, new_password_len};
|
||||
|
||||
@ -166,7 +160,7 @@ Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_00024Companion_changePas
|
||||
jbyte* returned_hash;
|
||||
GoSlice go_returned_hash = {NULL, 0, 0};
|
||||
if (!(*env)->IsSameObject(env, jreturned_hash, NULL)) {
|
||||
returned_hash_len = (*env)->GetArrayLength(env, jreturned_hash);
|
||||
returned_hash_len = (size_t) (*env)->GetArrayLength(env, jreturned_hash);
|
||||
returned_hash = (*env)->GetByteArrayElements(env, jreturned_hash, NULL);
|
||||
go_returned_hash.data = returned_hash;
|
||||
go_returned_hash.len = returned_hash_len;
|
||||
@ -303,7 +297,7 @@ Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1open_1write_1mod
|
||||
const char* file_path = (*env)->GetStringUTFChars(env, jfile_path, NULL);
|
||||
GoString go_file_path = {file_path, strlen(file_path)};
|
||||
|
||||
GoInt handleID = gcf_open_write_mode(sessionID, go_file_path, mode);
|
||||
GoInt handleID = gcf_open_write_mode(sessionID, go_file_path, (GoUint32) mode);
|
||||
|
||||
(*env)->ReleaseStringUTFChars(env, jfile_path, file_path);
|
||||
|
||||
@ -317,7 +311,7 @@ Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1write_1file(JNIE
|
||||
jbyte* buff = (*env)->GetByteArrayElements(env, jbuff, NULL);
|
||||
GoSlice go_buff = {buff+src_offset, length, length};
|
||||
|
||||
int written = gcf_write_file(sessionID, handleID, file_offset, go_buff);
|
||||
int written = gcf_write_file(sessionID, handleID, (GoUint64) file_offset, go_buff);
|
||||
|
||||
(*env)->ReleaseByteArrayElements(env, jbuff, buff, 0);
|
||||
|
||||
@ -331,7 +325,7 @@ Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1read_1file(JNIEn
|
||||
jbyte* buff = (*env)->GetByteArrayElements(env, jbuff, NULL);
|
||||
GoSlice go_buff = {buff+dst_offset, length, length};
|
||||
|
||||
int read = gcf_read_file(sessionID, handleID, file_offset, go_buff);
|
||||
int read = gcf_read_file(sessionID, handleID, (GoUint64) file_offset, go_buff);
|
||||
|
||||
(*env)->ReleaseByteArrayElements(env, jbuff, buff, 0);
|
||||
return read;
|
||||
@ -345,7 +339,7 @@ Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1truncate(JNIEnv
|
||||
const char* path = (*env)->GetStringUTFChars(env, jpath, NULL);
|
||||
GoString go_path = {path, strlen(path)};
|
||||
|
||||
GoUint8 result = gcf_truncate(sessionID, go_path, offset);
|
||||
GoUint8 result = gcf_truncate(sessionID, go_path, (GoUint64) offset);
|
||||
|
||||
(*env)->ReleaseStringUTFChars(env, jpath, path);
|
||||
return result;
|
||||
@ -377,7 +371,7 @@ Java_sushi_hardcore_droidfs_filesystems_GocryptfsVolume_native_1mkdir(JNIEnv *en
|
||||
const char* dir_path = (*env)->GetStringUTFChars(env, jdir_path, NULL);
|
||||
GoString go_dir_path = {dir_path, strlen(dir_path)};
|
||||
|
||||
GoUint8 result = gcf_mkdir(sessionID, go_dir_path, mode);
|
||||
GoUint8 result = gcf_mkdir(sessionID, go_dir_path, (GoUint32) mode);
|
||||
|
||||
(*env)->ReleaseStringUTFChars(env, jdir_path, dir_path);
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
<vector android:height="24dp" android:viewportHeight="500"
|
||||
android:viewportWidth="500" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?attr/colorAccent" android:pathData="M68.29,431.711c0,20.078 16.264,36.34 36.343,36.34h290.734c20.078,0 36.345,-16.262 36.345,-36.34V250c0,-20.079 -16.267,-36.342 -36.345,-36.342H177.317v-63.597c0,-40.157 32.525,-72.685 72.683,-72.685c40.158,0 72.685,32.528 72.685,72.685v4.541c0,12.538 10.176,22.715 22.711,22.715c12.537,0 22.717,-10.177 22.717,-22.715v-4.541c0,-65.232 -52.882,-118.111 -118.112,-118.111c-65.24,0 -118.111,52.879 -118.111,118.111v63.597h-27.256c-20.079,0 -36.343,16.263 -36.343,36.342V431.711zM213.658,313.599c0,-20.078 16.263,-36.341 36.342,-36.341s36.341,16.263 36.341,36.341c0,12.812 -6.634,24.079 -16.625,30.529c0,0 3.55,21.446 7.542,46.699c0,7.538 -6.087,13.625 -13.629,13.625h-27.258c-7.541,0 -13.627,-6.087 -13.627,-13.625l7.542,-46.699C220.294,337.678 213.658,326.41 213.658,313.599z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/icon_lock.xml
Normal file
5
app/src/main/res/drawable/icon_lock.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/icon_lock_open.xml
Normal file
5
app/src/main/res/drawable/icon_lock_open.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="?attr/colorAccent" android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z"/>
|
||||
</vector>
|
@ -8,21 +8,23 @@
|
||||
<RelativeLayout
|
||||
android:id="@+id/selectable_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
tools:ignore="UselessParent">
|
||||
tools:ignore="UselessParent"
|
||||
android:paddingEnd="4dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_icon"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"/>
|
||||
android:layout_height="50dp"
|
||||
android:src="@drawable/icon_volume"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_toEndOf="@id/image_icon"
|
||||
android:layout_toStartOf="@id/icon_fingerprint">
|
||||
android:layout_toStartOf="@id/layout_icons">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_volume_name"
|
||||
@ -63,16 +65,31 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_fingerprint"
|
||||
android:layout_width="26dp"
|
||||
android:layout_height="26dp"
|
||||
android:src="@drawable/icon_fingerprint"
|
||||
android:contentDescription="@string/password_hash_saved"
|
||||
android:visibility="gone"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginTop="11dp"
|
||||
android:layout_marginEnd="5dp"/>
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_icons"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_alignParentEnd="true">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_unlocked"
|
||||
android:layout_width="30dp"
|
||||
android:layout_height="30dp"
|
||||
android:src="@drawable/icon_lock_open"
|
||||
android:contentDescription="@string/volume_unlocked"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon_fingerprint"
|
||||
android:layout_width="26dp"
|
||||
android:layout_height="26dp"
|
||||
android:src="@drawable/icon_fingerprint"
|
||||
android:contentDescription="@string/password_hash_saved"
|
||||
android:visibility="gone"
|
||||
android:layout_marginStart="5dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
@ -30,9 +30,8 @@
|
||||
|
||||
<item
|
||||
android:id="@+id/decrypt"
|
||||
app:showAsAction="ifRoom"
|
||||
app:showAsAction="never"
|
||||
android:visible="false"
|
||||
android:icon="@drawable/icon_decrypt"
|
||||
android:title="@string/decrypt_files"/>
|
||||
|
||||
<item
|
||||
@ -72,6 +71,12 @@
|
||||
android:title="@string/sort_by"
|
||||
android:icon="@drawable/icon_sort"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/lock"
|
||||
app:showAsAction="ifRoom"
|
||||
android:title="@string/lock_volume"
|
||||
android:icon="@drawable/icon_lock"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/close"
|
||||
app:showAsAction="ifRoom"
|
||||
|
@ -19,6 +19,12 @@
|
||||
android:title="@string/sort_by"
|
||||
android:icon="@drawable/icon_sort"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/lock"
|
||||
app:showAsAction="ifRoom"
|
||||
android:title="@string/lock_volume"
|
||||
android:icon="@drawable/icon_lock"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/close"
|
||||
app:showAsAction="ifRoom"
|
||||
|
@ -38,6 +38,12 @@
|
||||
android:title="@string/sort_by"
|
||||
android:icon="@drawable/icon_sort"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/lock"
|
||||
app:showAsAction="ifRoom"
|
||||
android:title="@string/lock_volume"
|
||||
android:icon="@drawable/icon_lock"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/close"
|
||||
app:showAsAction="ifRoom"
|
||||
|
@ -9,6 +9,13 @@
|
||||
android:title="@string/select_all"
|
||||
android:icon="@drawable/icon_select_all"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/lock"
|
||||
app:showAsAction="ifRoom"
|
||||
android:visible="false"
|
||||
android:title="@string/lock"
|
||||
android:icon="@drawable/icon_lock"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/remove"
|
||||
app:showAsAction="ifRoom"
|
||||
|
@ -11,7 +11,6 @@
|
||||
<string name="mkdir">إنشاء مجلد</string>
|
||||
<string name="dir_empty">مجلد فارغ</string>
|
||||
<string name="warning">تحذير !</string>
|
||||
<string name="ask_close_volume">هل تريد إغلاق المجلد المشفر ?</string>
|
||||
<string name="ok">حسنا</string>
|
||||
<string name="cancel">إلغاء</string>
|
||||
<string name="enter_folder_name">اسم المجلد:</string>
|
||||
|
@ -11,7 +11,6 @@
|
||||
<string name="mkdir">Crear carpeta</string>
|
||||
<string name="dir_empty">Directorio vacío</string>
|
||||
<string name="warning">¡Advertencia!</string>
|
||||
<string name="ask_close_volume">¿Estás seguro de que quieres cerrar este volumen?</string>
|
||||
<string name="ok">Aceptar</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="enter_folder_name">Nombre de la carpeta:</string>
|
||||
|
@ -11,7 +11,6 @@
|
||||
<string name="mkdir">Criar pasta</string>
|
||||
<string name="dir_empty">Pasta vazia</string>
|
||||
<string name="warning">Aviso!</string>
|
||||
<string name="ask_close_volume">Tem certeza que quer fechar este volume?</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="enter_folder_name">Nome da pasta:</string>
|
||||
|
@ -10,7 +10,6 @@
|
||||
<string name="mkdir">Создать папку</string>
|
||||
<string name="dir_empty">Пусто</string>
|
||||
<string name="warning">Предупреждение!</string>
|
||||
<string name="ask_close_volume">Закрыть том?</string>
|
||||
<string name="cancel">Отмена</string>
|
||||
<string name="enter_folder_name">Имя папки:</string>
|
||||
<string name="error">Ошибка</string>
|
||||
|
@ -11,7 +11,7 @@
|
||||
<string name="mkdir">Create folder</string>
|
||||
<string name="dir_empty">Directory Empty</string>
|
||||
<string name="warning">Warning !</string>
|
||||
<string name="ask_close_volume">Are you sure you want to close this volume ?</string>
|
||||
<string name="ask_lock_volume">Are you sure you want to lock this volume ?</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="enter_folder_name">Folder Name:</string>
|
||||
@ -253,4 +253,7 @@
|
||||
<string name="remember_volume">Remember volume</string>
|
||||
<string name="open_volume">Open volume</string>
|
||||
<string name="choose_existing_volume">Please choose an existing volume</string>
|
||||
<string name="volume_unlocked">Volume unlocked</string>
|
||||
<string name="lock_volume">Lock volume</string>
|
||||
<string name="lock">Lock</string>
|
||||
</resources>
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/icon_decrypt"
|
||||
android:icon="@drawable/icon_lock_open"
|
||||
android:key="usf_decrypt"
|
||||
android:title="@string/usf_decrypt" />
|
||||
<SwitchPreference
|
||||
@ -31,7 +31,7 @@
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:icon="@drawable/icon_decrypt"
|
||||
android:icon="@drawable/icon_lock_open"
|
||||
android:key="usf_keep_open"
|
||||
android:title="@string/usf_keep_open" />
|
||||
|
||||
|
@ -5,7 +5,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.4.1'
|
||||
classpath 'com.android.tools.build:gradle:7.4.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user