RestrictedFileProvider & Playing media without disk writes

This commit is contained in:
Hardcore Sushi 2020-07-26 21:16:06 +02:00
parent ce74aad5cf
commit 24cfc1093e
31 changed files with 630 additions and 884 deletions

View File

@ -6,12 +6,16 @@ android {
compileSdkVersion 29
buildToolsVersion "30.0.0"
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
}
defaultConfig {
applicationId "sushi.hardcore.droidfs"
minSdkVersion 21
targetSdkVersion 28
targetSdkVersion 29
versionCode 1
versionName "1.0.1"
versionName "1.1.0"
ndk {
abiFilters 'x86_64', 'armeabi-v7a', 'arm64-v8a'
@ -35,7 +39,7 @@ android {
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.0'
implementation 'androidx.core:core-ktx:1.3.1'
implementation 'androidx.appcompat:appcompat:1.1.0'
testImplementation 'junit:junit:4.12'
@ -44,4 +48,6 @@ dependencies {
implementation 'com.github.clans:fab:1.6.4'
implementation 'com.jaredrummler:cyanea:1.0.2'
implementation 'com.github.bumptech.glide:glide:4.11.0'
implementation 'com.google.android.exoplayer:exoplayer-core:2.11.7'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.11.7'
}

View File

@ -771,7 +771,6 @@ func gcf_open_read_mode(sessionID int, path string) int {
}
defer syscall.Close(dirfd)
fd, err := syscallcompat.Openat(dirfd, cName, newFlags, 0)
// Handle a few specific errors
if err != nil {
return -1
}

View File

@ -2,6 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="sushi.hardcore.droidfs">
<permission
android:name="${applicationId}.WRITE_TEMPORARY_STORAGE"
android:protectionLevel="signature"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
@ -13,7 +17,8 @@
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"
android:requestLegacyExternalStorage="true">
<activity
android:name=".SettingsActivity"
android:label="@string/title_activity_settings"
@ -53,10 +58,10 @@
<activity android:name=".file_viewers.TextEditor" android:configChanges="screenSize|orientation" />
<provider
android:name=".provider.TemporaryFileProvider"
android:authorities="${applicationId}.tempstorage"
android:name=".provider.RestrictedFileProvider"
android:authorities="${applicationId}.temporary_provider"
android:exported="true"
android:writePermission="${applicationId}.READ_TEMPORARY_STORAGE" />
android:writePermission="${applicationId}.WRITE_TEMPORARY_STORAGE"/>
</application>
</manifest>

View File

@ -3,7 +3,6 @@ package sushi.hardcore.droidfs
import android.content.SharedPreferences
import android.os.Bundle
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.preference.PreferenceManager
import com.jaredrummler.cyanea.app.CyaneaAppCompatActivity

View File

@ -7,7 +7,6 @@ import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.WindowManager
import android.widget.AdapterView.OnItemClickListener
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_change_password.*
@ -18,7 +17,7 @@ import kotlinx.android.synthetic.main.activity_change_password.saved_path_listvi
import kotlinx.android.synthetic.main.toolbar.*
import sushi.hardcore.droidfs.adapters.SavedVolumesAdapter
import sushi.hardcore.droidfs.fingerprint_stuff.FingerprintPasswordHashSaver
import sushi.hardcore.droidfs.util.FilesUtils
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.GocryptfsVolume
import sushi.hardcore.droidfs.util.WidgetUtil
import sushi.hardcore.droidfs.util.Wiper
@ -80,7 +79,7 @@ class ChangePasswordActivity : BaseActivity() {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == PICK_DIRECTORY_REQUEST_CODE) {
if (data != null) {
val path = FilesUtils.getFullPathFromTreeUri(data.data, this)
val path = PathUtils.getFullPathFromTreeUri(data.data, this)
edit_volume_path.setText(path)
}
}

View File

@ -1,5 +1,6 @@
package sushi.hardcore.droidfs
import android.net.Uri
import java.io.File
class ConstValues {
@ -7,6 +8,7 @@ class ConstValues {
const val creator = "DroidFS"
const val saved_volumes_key = "saved_volumes"
const val sort_order_key = "sort_order"
val fakeUri = Uri.parse("fakeuri://droidfs")
const val wipe_passes = 2
const val seek_bar_inc = 200
private val fileExtensions = mapOf(

View File

@ -5,7 +5,6 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_create.*
import kotlinx.android.synthetic.main.activity_create.checkbox_remember_path
@ -15,7 +14,7 @@ import kotlinx.android.synthetic.main.activity_create.edit_volume_path
import kotlinx.android.synthetic.main.toolbar.*
import sushi.hardcore.droidfs.explorers.ExplorerActivity
import sushi.hardcore.droidfs.fingerprint_stuff.FingerprintPasswordHashSaver
import sushi.hardcore.droidfs.util.FilesUtils
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.GocryptfsVolume
import sushi.hardcore.droidfs.util.WidgetUtil
import sushi.hardcore.droidfs.util.Wiper
@ -57,7 +56,7 @@ class CreateActivity : BaseActivity() {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == PICK_DIRECTORY_REQUEST_CODE) {
if (data != null) {
val path = FilesUtils.getFullPathFromTreeUri(data.data, this)
val path = PathUtils.getFullPathFromTreeUri(data.data, this)
edit_volume_path.setText(path)
}
}

View File

@ -35,14 +35,14 @@ class MainActivity : BaseActivity() {
if (!storageAvailable) {
ColoredAlertDialog(this)
.setTitle(R.string.storage_unavailable)
.setMessage(getString(R.string.storage_unavailable_msg))
.setMessage(R.string.storage_unavailable_msg)
.setPositiveButton(R.string.ok
) { _, _ -> finish() }.show()
}
if (!sharedPrefs.getBoolean("alreadyLaunched", false)){
ColoredAlertDialog(this)
.setTitle(R.string.warning)
.setMessage(getString(R.string.usf_home_warning_msg))
.setMessage(R.string.usf_home_warning_msg)
.setCancelable(false)
.setPositiveButton(getString(R.string.see_unsafe_features)){ _, _ ->
val intent = Intent(this, SettingsActivity::class.java)
@ -65,7 +65,7 @@ class MainActivity : BaseActivity() {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED || grantResults[1] != PackageManager.PERMISSION_GRANTED) {
ColoredAlertDialog(this)
.setTitle(R.string.storage_perm_denied)
.setMessage(getString(R.string.storage_perm_denied_msg))
.setMessage(R.string.storage_perm_denied_msg)
.setPositiveButton(R.string.ok
) { _, _ -> finish() }.show()
}

View File

@ -5,7 +5,6 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import android.widget.AdapterView.OnItemClickListener
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_open.checkbox_remember_path
@ -19,7 +18,7 @@ import sushi.hardcore.droidfs.explorers.ExplorerActivity
import sushi.hardcore.droidfs.explorers.ExplorerActivityDrop
import sushi.hardcore.droidfs.explorers.ExplorerActivityPick
import sushi.hardcore.droidfs.fingerprint_stuff.FingerprintPasswordHashSaver
import sushi.hardcore.droidfs.util.FilesUtils
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.GocryptfsVolume
import sushi.hardcore.droidfs.util.WidgetUtil
import sushi.hardcore.droidfs.util.Wiper
@ -76,7 +75,7 @@ class OpenActivity : BaseActivity() {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == PICK_DIRECTORY_REQUEST_CODE) {
if (data != null) {
val path = FilesUtils.getFullPathFromTreeUri(data.data, this)
val path = PathUtils.getFullPathFromTreeUri(data.data, this)
edit_volume_path.setText(path)
}
}
@ -128,7 +127,7 @@ class OpenActivity : BaseActivity() {
} else {
ColoredAlertDialog(this)
.setTitle(R.string.open_volume_failed)
.setMessage(getString(R.string.open_failed_hash_msg))
.setMessage(R.string.open_failed_hash_msg)
.setPositiveButton(R.string.ok, null)
.show()
}

View File

@ -14,23 +14,23 @@ import androidx.core.content.ContextCompat
import sushi.hardcore.droidfs.ConstValues.Companion.getAssociatedDrawable
import sushi.hardcore.droidfs.explorers.ExplorerElement
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.util.FilesUtils
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.ThemeColor
import java.text.DateFormat
import java.util.*
class ExplorerElementAdapter(private val context: Context) : BaseAdapter() {
private val dateFormat: DateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, context.resources.configuration.locale)
private lateinit var explorer_elements: List<ExplorerElement>
private lateinit var explorerElements: List<ExplorerElement>
private val inflater: LayoutInflater = LayoutInflater.from(context)
val selectedItems: MutableList<Int> = ArrayList()
private val themeColor = ThemeColor.getThemeColor(context)
override fun getCount(): Int {
return explorer_elements.size
return explorerElements.size
}
override fun getItem(position: Int): ExplorerElement {
return explorer_elements[position]
return explorerElements[position]
}
override fun getItemId(position: Int): Long {
@ -55,7 +55,7 @@ class ExplorerElementAdapter(private val context: Context) : BaseAdapter() {
}
else -> {
textElementMtime.text = dateFormat.format(currentElement.mTime)
textElementSize.text = FilesUtils.formatSize(currentElement.size)
textElementSize.text = PathUtils.formatSize(currentElement.size)
drawableId = getAssociatedDrawable(currentElement.name)
}
}
@ -73,56 +73,55 @@ class ExplorerElementAdapter(private val context: Context) : BaseAdapter() {
fun onItemClick(position: Int) {
if (selectedItems.isNotEmpty()) {
if (!explorer_elements[position].isParentFolder) {
if (!explorerElements[position].isParentFolder) {
if (selectedItems.contains(position)) {
selectedItems.remove(position)
} else {
selectedItems.add(position)
}
notifyDataSetInvalidated()
notifyDataSetChanged()
}
}
}
fun onItemLongClick(position: Int) {
if (!explorer_elements[position].isParentFolder) {
if (!explorerElements[position].isParentFolder) {
if (!selectedItems.contains(position)) {
selectedItems.add(position)
} else {
selectedItems.remove(position)
}
notifyDataSetInvalidated()
notifyDataSetChanged()
}
}
fun selectAll() {
for (i in explorer_elements.indices) {
if (!selectedItems.contains(i) && !explorer_elements[i].isParentFolder) {
for (i in explorerElements.indices) {
if (!selectedItems.contains(i) && !explorerElements[i].isParentFolder) {
selectedItems.add(i)
}
}
notifyDataSetInvalidated()
notifyDataSetChanged()
}
fun unSelectAll() {
selectedItems.clear()
notifyDataSetInvalidated()
notifyDataSetChanged()
}
fun setExplorerElements(explorer_elements: List<ExplorerElement>) {
unSelectAll()
this.explorer_elements = explorer_elements
this.explorerElements = explorer_elements
}
val currentDirectoryTotalSize: Long
get() {
var total_size: Long = 0
for (e in explorer_elements) {
var totalSize: Long = 0
for (e in explorerElements) {
if (e.isRegularFile) {
total_size += e.size
totalSize += e.size
}
}
return total_size
return totalSize
}
}

View File

@ -1,7 +1,6 @@
package sushi.hardcore.droidfs.explorers
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
@ -29,50 +28,48 @@ import sushi.hardcore.droidfs.file_viewers.AudioPlayer
import sushi.hardcore.droidfs.file_viewers.ImageViewer
import sushi.hardcore.droidfs.file_viewers.TextEditor
import sushi.hardcore.droidfs.file_viewers.VideoPlayer
import sushi.hardcore.droidfs.provider.TemporaryFileProvider
import sushi.hardcore.droidfs.provider.RestrictedFileProvider
import sushi.hardcore.droidfs.util.ExternalProvider
import sushi.hardcore.droidfs.util.FilesUtils
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.GocryptfsVolume
import sushi.hardcore.droidfs.widgets.ColoredAlertDialog
import java.util.*
open class BaseExplorerActivity : BaseActivity() {
private lateinit var shared_prefs_editor: SharedPreferences.Editor
private lateinit var sort_modes_entries: Array<String>
private lateinit var sort_modes_values: Array<String>
private var current_sort_mode_index = 0
private lateinit var sortModesEntries: Array<String>
private lateinit var sortModesValues: Array<String>
private var currentSortModeIndex = 0
protected lateinit var gocryptfsVolume: GocryptfsVolume
private lateinit var volume_name: String
protected var current_path = ""
protected lateinit var explorer_elements: MutableList<ExplorerElement>
protected lateinit var explorer_adapter: ExplorerElementAdapter
private lateinit var volumeName: String
protected var currentDirectoryPath = ""
protected lateinit var explorerElements: MutableList<ExplorerElement>
protected lateinit var explorerAdapter: ExplorerElementAdapter
private var usf_open = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
usf_open = sharedPrefs.getBoolean("usf_open", false)
val intent = intent
volume_name = intent.getStringExtra("volume_name") ?: ""
volumeName = intent.getStringExtra("volume_name") ?: ""
val sessionID = intent.getIntExtra("sessionID", -1)
gocryptfsVolume = GocryptfsVolume(sessionID)
sort_modes_entries = resources.getStringArray(R.array.sort_orders_entries)
sort_modes_values = resources.getStringArray(R.array.sort_orders_values)
current_sort_mode_index = resources.getStringArray(R.array.sort_orders_values).indexOf(sharedPrefs.getString(ConstValues.sort_order_key, "name"))
shared_prefs_editor = sharedPrefs.edit()
sortModesEntries = resources.getStringArray(R.array.sort_orders_entries)
sortModesValues = resources.getStringArray(R.array.sort_orders_values)
currentSortModeIndex = resources.getStringArray(R.array.sort_orders_values).indexOf(sharedPrefs.getString(ConstValues.sort_order_key, "name"))
init()
setSupportActionBar(toolbar)
title = ""
title_text.text = getString(R.string.volume, volume_name)
explorer_adapter = ExplorerElementAdapter(this)
setCurrentPath(current_path)
list_explorer.adapter = explorer_adapter
title_text.text = getString(R.string.volume, volumeName)
explorerAdapter = ExplorerElementAdapter(this)
setCurrentPath(currentDirectoryPath)
list_explorer.adapter = explorerAdapter
list_explorer.onItemClickListener = OnItemClickListener { _, _, position, _ -> onExplorerItemClick(position) }
list_explorer.onItemLongClickListener = OnItemLongClickListener { _, _, position, _ ->
explorer_adapter.onItemLongClick(position)
explorerAdapter.onItemLongClick(position)
invalidateOptionsMenu()
true
}
refresher.setOnRefreshListener {
setCurrentPath(current_path)
setCurrentPath(currentDirectoryPath)
refresher.isRefreshing = false
}
}
@ -89,29 +86,29 @@ open class BaseExplorerActivity : BaseActivity() {
}
protected open fun onExplorerItemClick(position: Int) {
val wasSelecting = explorer_adapter.selectedItems.isNotEmpty()
explorer_adapter.onItemClick(position)
if (explorer_adapter.selectedItems.isEmpty()) {
val wasSelecting = explorerAdapter.selectedItems.isNotEmpty()
explorerAdapter.onItemClick(position)
if (explorerAdapter.selectedItems.isEmpty()) {
if (!wasSelecting) {
val full_path = FilesUtils.path_join(current_path, explorer_elements[position].name)
val fullPath = PathUtils.path_join(currentDirectoryPath, explorerElements[position].name)
when {
explorer_elements[position].isDirectory -> {
setCurrentPath(full_path)
explorerElements[position].isDirectory -> {
setCurrentPath(fullPath)
}
explorer_elements[position].isParentFolder -> {
setCurrentPath(FilesUtils.get_parent_path(current_path))
explorerElements[position].isParentFolder -> {
setCurrentPath(PathUtils.get_parent_path(currentDirectoryPath))
}
isImage(full_path) -> {
startFileViewer(ImageViewer::class.java, full_path)
isImage(fullPath) -> {
startFileViewer(ImageViewer::class.java, fullPath)
}
isVideo(full_path) -> {
startFileViewer(VideoPlayer::class.java, full_path)
isVideo(fullPath) -> {
startFileViewer(VideoPlayer::class.java, fullPath)
}
isText(full_path) -> {
startFileViewer(TextEditor::class.java, full_path)
isText(fullPath) -> {
startFileViewer(TextEditor::class.java, fullPath)
}
isAudio(full_path) -> {
startFileViewer(AudioPlayer::class.java, full_path)
isAudio(fullPath) -> {
startFileViewer(AudioPlayer::class.java, fullPath)
}
else -> {
val dialogListView = layoutInflater.inflate(R.layout.dialog_listview, null)
@ -125,10 +122,10 @@ open class BaseExplorerActivity : BaseActivity() {
.create()
listView.setOnItemClickListener{_, _, fileTypePosition, _ ->
when (adapter.getItem(fileTypePosition)){
"image" -> startFileViewer(ImageViewer::class.java, full_path)
"video" -> startFileViewer(VideoPlayer::class.java, full_path)
"audio" -> startFileViewer(AudioPlayer::class.java, full_path)
"text" -> startFileViewer(TextEditor::class.java, full_path)
"image" -> startFileViewer(ImageViewer::class.java, fullPath)
"video" -> startFileViewer(VideoPlayer::class.java, fullPath)
"audio" -> startFileViewer(AudioPlayer::class.java, fullPath)
"text" -> startFileViewer(TextEditor::class.java, fullPath)
}
dialog.dismiss()
}
@ -141,41 +138,42 @@ open class BaseExplorerActivity : BaseActivity() {
}
private fun sortExplorerElements() {
when (sort_modes_values[current_sort_mode_index]) {
when (sortModesValues[currentSortModeIndex]) {
"name" -> {
explorer_elements.sortWith(Comparator { o1, o2 -> o1.name.compareTo(o2.name) })
explorerElements.sortWith(Comparator { o1, o2 -> o1.name.compareTo(o2.name) })
}
"size" -> {
explorer_elements.sortWith(Comparator { o1, o2 -> (o1.size - o2.size).toInt() })
explorerElements.sortWith(Comparator { o1, o2 -> (o1.size - o2.size).toInt() })
}
"date" -> {
explorer_elements.sortWith(Comparator { o1, o2 -> o1.mTime.compareTo(o2.mTime) })
explorerElements.sortWith(Comparator { o1, o2 -> o1.mTime.compareTo(o2.mTime) })
}
"name_desc" -> {
explorer_elements.sortWith(Comparator { o1, o2 -> o2.name.compareTo(o1.name) })
explorerElements.sortWith(Comparator { o1, o2 -> o2.name.compareTo(o1.name) })
}
"size_desc" -> {
explorer_elements.sortWith(Comparator { o1, o2 -> (o2.size - o1.size).toInt() })
explorerElements.sortWith(Comparator { o1, o2 -> (o2.size - o1.size).toInt() })
}
"date_desc" -> {
explorer_elements.sortWith(Comparator { o1, o2 -> o2.mTime.compareTo(o1.mTime) })
explorerElements.sortWith(Comparator { o1, o2 -> o2.mTime.compareTo(o1.mTime) })
}
}
shared_prefs_editor.putString(ConstValues.sort_order_key, sort_modes_values[current_sort_mode_index])
shared_prefs_editor.apply()
val sharedPrefsEditor = sharedPrefs.edit()
sharedPrefsEditor.putString(ConstValues.sort_order_key, sortModesValues[currentSortModeIndex])
sharedPrefsEditor.apply()
}
protected fun setCurrentPath(path: String) {
explorer_elements = gocryptfsVolume.list_dir(path)
text_dir_empty.visibility = if (explorer_elements.size == 0) View.VISIBLE else View.INVISIBLE
explorerElements = gocryptfsVolume.list_dir(path)
text_dir_empty.visibility = if (explorerElements.size == 0) View.VISIBLE else View.INVISIBLE
sortExplorerElements()
if (path.isNotEmpty()) { //not root
explorer_elements.add(0, ExplorerElement("..", (-1).toShort(), -1, -1))
explorerElements.add(0, ExplorerElement("..", (-1).toShort(), -1, -1))
}
explorer_adapter.setExplorerElements(explorer_elements)
current_path = path
current_path_text.text = getString(R.string.location, current_path)
total_size_text.text = getString(R.string.total_size, FilesUtils.formatSize(explorer_adapter.currentDirectoryTotalSize))
explorerAdapter.setExplorerElements(explorerElements)
currentDirectoryPath = path
current_path_text.text = getString(R.string.location, currentDirectoryPath)
total_size_text.text = getString(R.string.total_size, PathUtils.formatSize(explorerAdapter.currentDirectoryTotalSize))
}
private fun askCloseVolume() {
@ -193,19 +191,19 @@ open class BaseExplorerActivity : BaseActivity() {
protected open fun closeVolumeOnDestroy() {
gocryptfsVolume.close()
TemporaryFileProvider.wipeAll() //additional security
RestrictedFileProvider.wipeAll() //additional security
}
override fun onBackPressed() {
if (explorer_adapter.selectedItems.isEmpty()) {
val parent_path = FilesUtils.get_parent_path(current_path)
if (parent_path == current_path) {
if (explorerAdapter.selectedItems.isEmpty()) {
val parentPath = PathUtils.get_parent_path(currentDirectoryPath)
if (parentPath == currentDirectoryPath) {
askCloseVolume()
} else {
setCurrentPath(FilesUtils.get_parent_path(current_path))
setCurrentPath(PathUtils.get_parent_path(currentDirectoryPath))
}
} else {
explorer_adapter.unSelectAll()
explorerAdapter.unSelectAll()
invalidateOptionsMenu()
}
}
@ -214,14 +212,14 @@ open class BaseExplorerActivity : BaseActivity() {
if (folder_name.isEmpty()) {
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
} else {
if (!gocryptfsVolume.mkdir(FilesUtils.path_join(current_path, folder_name))) {
if (!gocryptfsVolume.mkdir(PathUtils.path_join(currentDirectoryPath, folder_name))) {
ColoredAlertDialog(this)
.setTitle(R.string.error)
.setMessage(R.string.error_mkdir)
.setPositiveButton(R.string.ok, null)
.show()
} else {
setCurrentPath(current_path)
setCurrentPath(currentDirectoryPath)
invalidateOptionsMenu()
}
}
@ -254,14 +252,14 @@ open class BaseExplorerActivity : BaseActivity() {
if (new_name.isEmpty()) {
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
} else {
if (!gocryptfsVolume.rename(FilesUtils.path_join(current_path, old_name), FilesUtils.path_join(current_path, new_name))) {
if (!gocryptfsVolume.rename(PathUtils.path_join(currentDirectoryPath, old_name), PathUtils.path_join(currentDirectoryPath, new_name))) {
ColoredAlertDialog(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.rename_failed, old_name))
.setPositiveButton(R.string.ok, null)
.show()
} else {
setCurrentPath(current_path)
setCurrentPath(currentDirectoryPath)
invalidateOptionsMenu()
}
}
@ -272,7 +270,7 @@ open class BaseExplorerActivity : BaseActivity() {
if (usf_open){
menu.findItem(R.id.explorer_menu_external_open)?.isVisible = false
}
val selectedItems = explorer_adapter.selectedItems
val selectedItems = explorerAdapter.selectedItems
if (selectedItems.isEmpty()){
toolbar.navigationIcon = null
menu.findItem(R.id.explorer_menu_close).isVisible = true
@ -283,7 +281,7 @@ open class BaseExplorerActivity : BaseActivity() {
menu.findItem(R.id.explorer_menu_sort).isVisible = false
if (selectedItems.size == 1) {
menu.findItem(R.id.explorer_menu_rename).isVisible = true
if (usf_open && explorer_elements[selectedItems[0]].isRegularFile) {
if (usf_open && explorerElements[selectedItems[0]].isRegularFile) {
menu.findItem(R.id.explorer_menu_external_open)?.isVisible = true
}
}
@ -293,39 +291,39 @@ open class BaseExplorerActivity : BaseActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
explorer_adapter.unSelectAll()
explorerAdapter.unSelectAll()
invalidateOptionsMenu()
true
}
R.id.explorer_menu_sort -> {
ColoredAlertDialog(this)
.setTitle(R.string.sort_order)
.setSingleChoiceItems(sort_modes_entries, current_sort_mode_index) { dialog, which ->
current_sort_mode_index = which
setCurrentPath(current_path)
.setSingleChoiceItems(sortModesEntries, currentSortModeIndex) { dialog, which ->
currentSortModeIndex = which
setCurrentPath(currentDirectoryPath)
dialog.dismiss()
}.show()
true
}
R.id.explorer_menu_rename -> {
val dialog_edit_text_view = layoutInflater.inflate(R.layout.dialog_edit_text, null)
val old_name = explorer_elements[explorer_adapter.selectedItems[0]].name
val dialog_edit_text = dialog_edit_text_view.findViewById<EditText>(R.id.dialog_edit_text)
dialog_edit_text.setText(old_name)
dialog_edit_text.selectAll()
val dialogEditTextView = layoutInflater.inflate(R.layout.dialog_edit_text, null)
val oldName = explorerElements[explorerAdapter.selectedItems[0]].name
val dialogEditText = dialogEditTextView.findViewById<EditText>(R.id.dialog_edit_text)
dialogEditText.setText(oldName)
dialogEditText.selectAll()
val dialog = ColoredAlertDialog(this)
.setView(dialog_edit_text_view)
.setView(dialogEditTextView)
.setTitle(R.string.rename_title)
.setPositiveButton(R.string.ok) { _, _ ->
val new_name = dialog_edit_text.text.toString()
rename(old_name, new_name)
val newName = dialogEditText.text.toString()
rename(oldName, newName)
}
.setNegativeButton(R.string.cancel, null)
.create()
dialog_edit_text.setOnEditorActionListener { _, _, _ ->
val new_name = dialog_edit_text.text.toString()
dialogEditText.setOnEditorActionListener { _, _, _ ->
val newName = dialogEditText.text.toString()
dialog.dismiss()
rename(old_name, new_name)
rename(oldName, newName)
true
}
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
@ -334,8 +332,8 @@ open class BaseExplorerActivity : BaseActivity() {
}
R.id.explorer_menu_external_open -> {
if (usf_open){
ExternalProvider.open(this, gocryptfsVolume, FilesUtils.path_join(current_path, explorer_elements[explorer_adapter.selectedItems[0]].name))
explorer_adapter.unSelectAll()
ExternalProvider.open(this, gocryptfsVolume, PathUtils.path_join(currentDirectoryPath, explorerElements[explorerAdapter.selectedItems[0]].name))
explorerAdapter.unSelectAll()
invalidateOptionsMenu()
}
true
@ -357,6 +355,6 @@ open class BaseExplorerActivity : BaseActivity() {
override fun onResume() {
super.onResume()
ExternalProvider.clear_cache(this)
ExternalProvider.removeFiles(this)
}
}

View File

@ -14,7 +14,7 @@ import kotlinx.android.synthetic.main.activity_explorer.*
import sushi.hardcore.droidfs.OpenActivity
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.util.ExternalProvider
import sushi.hardcore.droidfs.util.FilesUtils
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.util.GocryptfsVolume
import sushi.hardcore.droidfs.util.Wiper
import sushi.hardcore.droidfs.widgets.ColoredAlertDialog
@ -37,16 +37,16 @@ class ExplorerActivity : BaseExplorerActivity() {
if (fileName.isEmpty()) {
Toast.makeText(this, R.string.error_filename_empty, Toast.LENGTH_SHORT).show()
} else {
val handleID = gocryptfsVolume.open_write_mode(FilesUtils.path_join(current_path, fileName))
val handleID = gocryptfsVolume.open_write_mode(PathUtils.path_join(currentDirectoryPath, fileName))
if (handleID == -1) {
ColoredAlertDialog(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.file_creation_failed))
.setMessage(R.string.file_creation_failed)
.setPositiveButton(R.string.ok, null)
.show()
} else {
gocryptfsVolume.close_file(handleID)
setCurrentPath(current_path)
setCurrentPath(currentDirectoryPath)
invalidateOptionsMenu()
}
}
@ -110,7 +110,7 @@ class ExplorerActivity : BaseExplorerActivity() {
if (uris.isNotEmpty()){
var success = true
for (uri in uris) {
val dstPath = FilesUtils.path_join(current_path, FilesUtils.getFilenameFromURI(this, uri))
val dstPath = PathUtils.path_join(currentDirectoryPath, PathUtils.getFilenameFromURI(this, uri))
contentResolver.openInputStream(uri)?.let {
success = gocryptfsVolume.import_file(it, dstPath)
}
@ -154,32 +154,32 @@ class ExplorerActivity : BaseExplorerActivity() {
.setNegativeButton(getString(R.string.no), null)
.show()
}
setCurrentPath(current_path)
setCurrentPath(currentDirectoryPath)
}
}
} else if (requestCode == PICK_DIRECTORY_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK && data != null) {
val uri = data.data
val output_dir = FilesUtils.getFullPathFromTreeUri(uri, this)
var failed_item: String? = null
for (i in explorer_adapter.selectedItems) {
val element = explorer_adapter.getItem(i)
val full_path = FilesUtils.path_join(current_path, element.name)
failed_item = if (element.isDirectory) {
recursive_export_directory(full_path, output_dir)
val outputDir = PathUtils.getFullPathFromTreeUri(uri, this)
var failedItem: String? = null
for (i in explorerAdapter.selectedItems) {
val element = explorerAdapter.getItem(i)
val fullPath = PathUtils.path_join(currentDirectoryPath, element.name)
failedItem = if (element.isDirectory) {
recursiveExportDirectory(fullPath, outputDir)
} else {
if (gocryptfsVolume.export_file(full_path, FilesUtils.path_join(output_dir, element.name))) null else full_path
if (gocryptfsVolume.export_file(fullPath, PathUtils.path_join(outputDir, element.name))) null else fullPath
}
if (failed_item != null) {
if (failedItem != null) {
ColoredAlertDialog(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.export_failed, failed_item))
.setMessage(getString(R.string.export_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
break
}
}
if (failed_item == null) {
if (failedItem == null) {
ColoredAlertDialog(this)
.setTitle(R.string.success_export)
.setMessage(R.string.success_export_msg)
@ -187,33 +187,33 @@ class ExplorerActivity : BaseExplorerActivity() {
.show()
}
}
explorer_adapter.unSelectAll()
explorerAdapter.unSelectAll()
invalidateOptionsMenu()
} else if (requestCode == PICK_OTHER_VOLUME_ITEMS_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK && data != null) {
val remote_sessionID = data.getIntExtra("sessionID", -1)
val remote_gocryptfsVolume = GocryptfsVolume(remote_sessionID)
val remoteSessionID = data.getIntExtra("sessionID", -1)
val remoteGocryptfsVolume = GocryptfsVolume(remoteSessionID)
val path = data.getStringExtra("path")
var failed_item: String? = null
var failedItem: String? = null
if (path == null) {
val paths = data.getStringArrayListExtra("paths")
val types = data.getIntegerArrayListExtra("types")
if (types != null && paths != null){
for (i in paths.indices) {
failed_item = if (types[i] == 0) { //directory
recursive_import_directory_from_other_volume(remote_gocryptfsVolume, paths[i], current_path)
failedItem = if (types[i] == 0) { //directory
recursiveImportDirectoryFromOtherVolume(remoteGocryptfsVolume, paths[i], currentDirectoryPath)
} else {
if (import_file_from_other_volume(remote_gocryptfsVolume, paths[i], current_path)) null else paths[i]
if (importFileFromOtherVolume(remoteGocryptfsVolume, paths[i], currentDirectoryPath)) null else paths[i]
}
if (failed_item != null) {
if (failedItem != null) {
break
}
}
}
} else {
failed_item = if (import_file_from_other_volume(remote_gocryptfsVolume, path, current_path)) null else path
failedItem = if (importFileFromOtherVolume(remoteGocryptfsVolume, path, currentDirectoryPath)) null else path
}
if (failed_item == null) {
if (failedItem == null) {
ColoredAlertDialog(this)
.setTitle(R.string.success_import)
.setMessage(R.string.success_import_msg)
@ -222,12 +222,12 @@ class ExplorerActivity : BaseExplorerActivity() {
} else {
ColoredAlertDialog(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.import_failed, failed_item))
.setMessage(getString(R.string.import_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
}
remote_gocryptfsVolume.close()
setCurrentPath(current_path)
remoteGocryptfsVolume.close()
setCurrentPath(currentDirectoryPath)
}
}
}
@ -238,14 +238,14 @@ class ExplorerActivity : BaseExplorerActivity() {
if (usf_share){
menu.findItem(R.id.explorer_menu_share).isVisible = false
}
val any_item_selected = explorer_adapter.selectedItems.isNotEmpty()
menu.findItem(R.id.explorer_menu_select_all).isVisible = any_item_selected
menu.findItem(R.id.explorer_menu_delete).isVisible = any_item_selected
menu.findItem(R.id.explorer_menu_decrypt).isVisible = any_item_selected && usf_decrypt
if (any_item_selected && usf_share){
val anyItemSelected = explorerAdapter.selectedItems.isNotEmpty()
menu.findItem(R.id.explorer_menu_select_all).isVisible = anyItemSelected
menu.findItem(R.id.explorer_menu_delete).isVisible = anyItemSelected
menu.findItem(R.id.explorer_menu_decrypt).isVisible = anyItemSelected && usf_decrypt
if (anyItemSelected && usf_share){
var containsDir = false
for (i in explorer_adapter.selectedItems) {
if (explorer_elements[i].isDirectory) {
for (i in explorerAdapter.selectedItems) {
if (explorerElements[i].isDirectory) {
containsDir = true
break
}
@ -260,32 +260,32 @@ class ExplorerActivity : BaseExplorerActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.explorer_menu_select_all -> {
explorer_adapter.selectAll()
explorerAdapter.selectAll()
invalidateOptionsMenu()
true
}
R.id.explorer_menu_delete -> {
val size = explorer_adapter.selectedItems.size
val size = explorerAdapter.selectedItems.size
val dialog = ColoredAlertDialog(this)
dialog.setTitle(R.string.warning)
dialog.setPositiveButton(R.string.ok) { _, _ -> remove_selected_items() }
dialog.setPositiveButton(R.string.ok) { _, _ -> removeSelectedItems() }
dialog.setNegativeButton(R.string.cancel, null)
if (size > 1) {
dialog.setMessage(getString(R.string.multiple_delete_confirm, explorer_adapter.selectedItems.size.toString()))
dialog.setMessage(getString(R.string.multiple_delete_confirm, explorerAdapter.selectedItems.size.toString()))
} else {
dialog.setMessage(getString(R.string.single_delete_confirm, explorer_adapter.getItem(explorer_adapter.selectedItems[0]).name))
dialog.setMessage(getString(R.string.single_delete_confirm, explorerAdapter.getItem(explorerAdapter.selectedItems[0]).name))
}
dialog.show()
true
}
R.id.explorer_menu_share -> {
val paths: MutableList<String> = ArrayList()
for (i in explorer_adapter.selectedItems) {
val e = explorer_elements[i]
paths.add(FilesUtils.path_join(current_path, e.name))
for (i in explorerAdapter.selectedItems) {
val e = explorerElements[i]
paths.add(PathUtils.path_join(currentDirectoryPath, e.name))
}
ExternalProvider.share(this, gocryptfsVolume, paths)
explorer_adapter.unSelectAll()
explorerAdapter.unSelectAll()
invalidateOptionsMenu()
true
}
@ -298,18 +298,18 @@ class ExplorerActivity : BaseExplorerActivity() {
}
}
private fun import_file_from_other_volume(remote_gocryptfsVolume: GocryptfsVolume, full_path: String, output_dir: String): Boolean {
val output_path = FilesUtils.path_join(output_dir, File(full_path).name)
private fun importFileFromOtherVolume(remote_gocryptfsVolume: GocryptfsVolume, full_path: String, output_dir: String): Boolean {
val outputPath = PathUtils.path_join(output_dir, File(full_path).name)
var success = true
val src_handleID = remote_gocryptfsVolume.open_read_mode(full_path)
if (src_handleID != -1) {
val dst_handleID = gocryptfsVolume.open_write_mode(output_path)
if (dst_handleID != -1) {
val srcHandleID = remote_gocryptfsVolume.open_read_mode(full_path)
if (srcHandleID != -1) {
val dstHandleID = gocryptfsVolume.open_write_mode(outputPath)
if (dstHandleID != -1) {
var length: Int
val io_buffer = ByteArray(GocryptfsVolume.DefaultBS)
val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS)
var offset: Long = 0
while (remote_gocryptfsVolume.read_file(src_handleID, offset, io_buffer).also { length = it } > 0){
val written = gocryptfsVolume.write_file(dst_handleID, offset, io_buffer, length).toLong()
while (remote_gocryptfsVolume.read_file(srcHandleID, offset, ioBuffer).also { length = it } > 0){
val written = gocryptfsVolume.write_file(dstHandleID, offset, ioBuffer, length).toLong()
if (written == length.toLong()) {
offset += length.toLong()
} else {
@ -317,46 +317,46 @@ class ExplorerActivity : BaseExplorerActivity() {
break
}
}
gocryptfsVolume.close_file(dst_handleID)
gocryptfsVolume.close_file(dstHandleID)
}
remote_gocryptfsVolume.close_file(src_handleID)
remote_gocryptfsVolume.close_file(srcHandleID)
}
return success
}
private fun recursive_import_directory_from_other_volume(remote_gocryptfsVolume: GocryptfsVolume, remote_directory_path: String, output_dir: String): String? {
val directory_path = FilesUtils.path_join(output_dir, File(remote_directory_path).name)
if (!gocryptfsVolume.path_exists(directory_path)) {
if (!gocryptfsVolume.mkdir(directory_path)) {
return directory_path
private fun recursiveImportDirectoryFromOtherVolume(remote_gocryptfsVolume: GocryptfsVolume, remote_directory_path: String, output_dir: String): String? {
val directoryPath = PathUtils.path_join(output_dir, File(remote_directory_path).name)
if (!gocryptfsVolume.path_exists(directoryPath)) {
if (!gocryptfsVolume.mkdir(directoryPath)) {
return directoryPath
}
}
val explorer_elements = remote_gocryptfsVolume.list_dir(remote_directory_path)
for (e in explorer_elements) {
val full_path = FilesUtils.path_join(remote_directory_path, e.name)
val explorerElements = remote_gocryptfsVolume.list_dir(remote_directory_path)
for (e in explorerElements) {
val fullPath = PathUtils.path_join(remote_directory_path, e.name)
if (e.isDirectory) {
val failed_item = recursive_import_directory_from_other_volume(remote_gocryptfsVolume, full_path, directory_path)
failed_item?.let { return it }
val failedItem = recursiveImportDirectoryFromOtherVolume(remote_gocryptfsVolume, fullPath, directoryPath)
failedItem?.let { return it }
} else {
if (!import_file_from_other_volume(remote_gocryptfsVolume, full_path, directory_path)) {
return full_path
if (!importFileFromOtherVolume(remote_gocryptfsVolume, fullPath, directoryPath)) {
return fullPath
}
}
}
return null
}
private fun recursive_export_directory(plain_directory_path: String, output_dir: String?): String? {
if (File(FilesUtils.path_join(output_dir, plain_directory_path)).mkdir()) {
val explorer_elements = gocryptfsVolume.list_dir(plain_directory_path)
for (e in explorer_elements) {
val full_path = FilesUtils.path_join(plain_directory_path, e.name)
private fun recursiveExportDirectory(plain_directory_path: String, output_dir: String?): String? {
if (File(PathUtils.path_join(output_dir, plain_directory_path)).mkdir()) {
val explorerElements = gocryptfsVolume.list_dir(plain_directory_path)
for (e in explorerElements) {
val fullPath = PathUtils.path_join(plain_directory_path, e.name)
if (e.isDirectory) {
val failed_item = recursive_export_directory(full_path, output_dir)
failed_item?.let { return it }
val failedItem = recursiveExportDirectory(fullPath, output_dir)
failedItem?.let { return it }
} else {
if (!gocryptfsVolume.export_file(full_path, FilesUtils.path_join(output_dir, full_path))) {
return full_path
if (!gocryptfsVolume.export_file(fullPath, PathUtils.path_join(output_dir, fullPath))) {
return fullPath
}
}
}
@ -365,16 +365,16 @@ class ExplorerActivity : BaseExplorerActivity() {
return output_dir
}
private fun recursive_remove_directory(plain_directory_path: String): String? {
val explorer_elements = gocryptfsVolume.list_dir(plain_directory_path)
for (e in explorer_elements) {
val full_path = FilesUtils.path_join(plain_directory_path, e.name)
private fun recursiveRemoveDirectory(plain_directory_path: String): String? {
val explorerElements = gocryptfsVolume.list_dir(plain_directory_path)
for (e in explorerElements) {
val fullPath = PathUtils.path_join(plain_directory_path, e.name)
if (e.isDirectory) {
val result = recursive_remove_directory(full_path)
val result = recursiveRemoveDirectory(fullPath)
result?.let { return it }
} else {
if (!gocryptfsVolume.remove_file(full_path)) {
return full_path
if (!gocryptfsVolume.remove_file(fullPath)) {
return fullPath
}
}
}
@ -385,30 +385,30 @@ class ExplorerActivity : BaseExplorerActivity() {
}
}
private fun remove_selected_items() {
var failed_item: String? = null
for (i in explorer_adapter.selectedItems) {
val element = explorer_adapter.getItem(i)
val full_path = FilesUtils.path_join(current_path, element.name)
private fun removeSelectedItems() {
var failedItem: String? = null
for (i in explorerAdapter.selectedItems) {
val element = explorerAdapter.getItem(i)
val fullPath = PathUtils.path_join(currentDirectoryPath, element.name)
if (element.isDirectory) {
val result = recursive_remove_directory(full_path)
result?.let{ failed_item = it }
val result = recursiveRemoveDirectory(fullPath)
result?.let{ failedItem = it }
} else {
if (!gocryptfsVolume.remove_file(full_path)) {
failed_item = full_path
if (!gocryptfsVolume.remove_file(fullPath)) {
failedItem = fullPath
}
}
if (failed_item != null) {
if (failedItem != null) {
ColoredAlertDialog(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.remove_failed, failed_item))
.setMessage(getString(R.string.remove_failed, failedItem))
.setPositiveButton(R.string.ok, null)
.show()
break
}
}
explorer_adapter.unSelectAll()
explorerAdapter.unSelectAll()
invalidateOptionsMenu()
setCurrentPath(current_path) //refresh
setCurrentPath(currentDirectoryPath) //refresh
}
}

View File

@ -5,7 +5,7 @@ import android.net.Uri
import android.view.Menu
import android.view.MenuItem
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.util.FilesUtils
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.ColoredAlertDialog
class ExplorerActivityDrop : BaseExplorerActivity() {
@ -16,7 +16,7 @@ class ExplorerActivityDrop : BaseExplorerActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.explorer_drop, menu)
handleMenuItems(menu)
menu.findItem(R.id.explorer_menu_validate).isVisible = explorer_adapter.selectedItems.isEmpty()
menu.findItem(R.id.explorer_menu_validate).isVisible = explorerAdapter.selectedItems.isEmpty()
return true
}
@ -31,13 +31,13 @@ class ExplorerActivityDrop : BaseExplorerActivity() {
if (extras != null && extras.containsKey(Intent.EXTRA_STREAM)){
if (intent.action == Intent.ACTION_SEND) {
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
val output_path = FilesUtils.path_join(current_path, FilesUtils.getFilenameFromURI(this, uri))
val output_path = PathUtils.path_join(currentDirectoryPath, PathUtils.getFilenameFromURI(this, uri))
error_msg = if (gocryptfsVolume.import_file(this, uri, output_path)) null else getString(R.string.import_failed, output_path)
} else if (intent.action == Intent.ACTION_SEND_MULTIPLE) {
val uris = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
if (uris != null){
for (uri in uris) {
val output_path = FilesUtils.path_join(current_path, FilesUtils.getFilenameFromURI(this, uri))
val output_path = PathUtils.path_join(currentDirectoryPath, PathUtils.getFilenameFromURI(this, uri))
if (!gocryptfsVolume.import_file(this, uri, output_path)) {
error_msg = getString(R.string.import_failed, output_path)
break

View File

@ -5,8 +5,8 @@ import android.content.Intent
import android.view.Menu
import android.view.MenuItem
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.provider.TemporaryFileProvider
import sushi.hardcore.droidfs.util.FilesUtils
import sushi.hardcore.droidfs.provider.RestrictedFileProvider
import sushi.hardcore.droidfs.util.PathUtils
import java.util.*
class ExplorerActivityPick : BaseExplorerActivity() {
@ -17,21 +17,21 @@ class ExplorerActivityPick : BaseExplorerActivity() {
}
override fun onExplorerItemClick(position: Int) {
val wasSelecting = explorer_adapter.selectedItems.isNotEmpty()
explorer_adapter.onItemClick(position)
if (explorer_adapter.selectedItems.isEmpty()) {
val wasSelecting = explorerAdapter.selectedItems.isNotEmpty()
explorerAdapter.onItemClick(position)
if (explorerAdapter.selectedItems.isEmpty()) {
if (!wasSelecting) {
val full_path = FilesUtils.path_join(current_path, explorer_elements[position].name)
val full_path = PathUtils.path_join(currentDirectoryPath, explorerElements[position].name)
when {
explorer_elements[position].isDirectory -> {
explorerElements[position].isDirectory -> {
setCurrentPath(full_path)
}
explorer_elements[position].isParentFolder -> {
setCurrentPath(FilesUtils.get_parent_path(current_path))
explorerElements[position].isParentFolder -> {
setCurrentPath(PathUtils.get_parent_path(currentDirectoryPath))
}
else -> {
result_intent.putExtra("path", full_path)
return_activity_result()
returnActivityResult()
}
}
}
@ -42,7 +42,7 @@ class ExplorerActivityPick : BaseExplorerActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.explorer_pick, menu)
handleMenuItems(menu)
val any_item_selected = explorer_adapter.selectedItems.isNotEmpty()
val any_item_selected = explorerAdapter.selectedItems.isNotEmpty()
menu.findItem(R.id.explorer_menu_select_all).isVisible = any_item_selected
menu.findItem(R.id.explorer_menu_validate).isVisible = any_item_selected
return true
@ -51,35 +51,35 @@ class ExplorerActivityPick : BaseExplorerActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.explorer_menu_select_all -> {
explorer_adapter.selectAll()
explorerAdapter.selectAll()
invalidateOptionsMenu()
true
}
R.id.explorer_menu_validate -> {
val paths = ArrayList<String>()
val types = ArrayList<Int>()
for (i in explorer_adapter.selectedItems) {
val e = explorer_elements[i]
paths.add(FilesUtils.path_join(current_path, e.name))
for (i in explorerAdapter.selectedItems) {
val e = explorerElements[i]
paths.add(PathUtils.path_join(currentDirectoryPath, e.name))
types.add(e.elementType.toInt())
}
result_intent.putStringArrayListExtra("paths", paths)
result_intent.putIntegerArrayListExtra("types", types)
return_activity_result()
returnActivityResult()
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun return_activity_result() {
private fun returnActivityResult() {
setResult(Activity.RESULT_OK, result_intent)
finish()
}
override fun closeVolumeOnDestroy() {
//don't close volume
TemporaryFileProvider.wipeAll()
RestrictedFileProvider.wipeAll()
}
override fun closeVolumeOnUserExit() {

View File

@ -1,20 +1,13 @@
package sushi.hardcore.droidfs.file_viewers
import android.media.MediaPlayer
import android.os.Handler
import android.widget.SeekBar
import androidx.core.content.ContextCompat
import com.google.android.exoplayer2.SimpleExoPlayer
import kotlinx.android.synthetic.main.activity_audio_player.*
import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.widgets.ColoredAlertDialog
import java.io.File
import java.io.IOException
class AudioPlayer: FileViewerActivity(){
private lateinit var player: MediaPlayer
private var isPrepared = false
class AudioPlayer: MediaPlayer(){
override fun viewFile() {
super.viewFile()
setContentView(R.layout.activity_audio_player)
val filename = File(filePath).name
val pos = filename.lastIndexOf('.')
@ -23,65 +16,9 @@ class AudioPlayer: FileViewerActivity(){
} else {
filename
}
val tmpFileUri = exportFile(filePath)
tmpFileUri?.let {
player = MediaPlayer()
player.setDataSource(this, tmpFileUri)
try {
player.prepare()
isPrepared = true
} catch (e: IOException){
ColoredAlertDialog(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.media_player_prepare_failed))
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> finish() }
.show()
}
if (isPrepared){
player.isLooping = true
button_pause.setOnClickListener {
if (player.isPlaying) {
player.pause()
button_pause.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.icon_play))
} else {
player.start()
button_pause.setImageDrawable(ContextCompat.getDrawable(this, R.drawable.icon_pause))
}
}
button_stop.setOnClickListener { finish() }
seekbar.max = player.duration / ConstValues.seek_bar_inc
val handler = Handler()
runOnUiThread(object : Runnable {
override fun run() {
if (isPrepared) {
seekbar.progress = player.currentPosition / ConstValues.seek_bar_inc
}
handler.postDelayed(this, ConstValues.seek_bar_inc.toLong())
}
})
seekbar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
if (::player.isInitialized && fromUser) {
player.seekTo(progress * ConstValues.seek_bar_inc)
}
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
player.start()
}
}
}
override fun onDestroy() {
super.onDestroy()
if (::player.isInitialized) {
if (player.isPlaying) {
player.stop()
}
isPrepared = false
player.release()
}
override fun bindPlayer(player: SimpleExoPlayer) {
audio_controller.player = player
}
}

View File

@ -1,21 +1,13 @@
package sushi.hardcore.droidfs.file_viewers
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.WindowManager
import androidx.preference.PreferenceManager
import sushi.hardcore.droidfs.BaseActivity
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.provider.TemporaryFileProvider
import sushi.hardcore.droidfs.util.GocryptfsVolume
import sushi.hardcore.droidfs.util.Wiper
import sushi.hardcore.droidfs.widgets.ColoredAlertDialog
import java.io.File
import java.util.ArrayList
abstract class FileViewerActivity: BaseActivity() {
private var cachedFiles: MutableList<Uri> = ArrayList()
lateinit var gocryptfsVolume: GocryptfsVolume
lateinit var filePath: String
override fun onCreate(savedInstanceState: Bundle?) {
@ -23,15 +15,17 @@ abstract class FileViewerActivity: BaseActivity() {
filePath = intent.getStringExtra("path")!!
val sessionID = intent.getIntExtra("sessionID", -1)
gocryptfsVolume = GocryptfsVolume(sessionID)
toggleFullscreen()
hideSystemUi()
viewFile()
}
open fun toggleFullscreen(){
var uiOptions = window.decorView.systemUiVisibility
//uiOptions ^= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
uiOptions = uiOptions xor View.SYSTEM_UI_FLAG_FULLSCREEN
uiOptions = uiOptions xor View.SYSTEM_UI_FLAG_IMMERSIVE
window.decorView.systemUiVisibility = uiOptions
open fun hideSystemUi(){
window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LOW_PROFILE or
View.SYSTEM_UI_FLAG_FULLSCREEN/* or
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION*/
}
abstract fun viewFile()
fun loadWholeFile(path: String): ByteArray? {
@ -43,10 +37,10 @@ abstract class FileViewerActivity: BaseActivity() {
val handleID = gocryptfsVolume.open_read_mode(path)
if (handleID != -1) {
var offset: Long = 0
val io_buffer = ByteArray(GocryptfsVolume.DefaultBS)
val ioBuffer = ByteArray(GocryptfsVolume.DefaultBS)
var length: Int
while (gocryptfsVolume.read_file(handleID, offset, io_buffer).also { length = it } > 0){
System.arraycopy(io_buffer, 0, fileBuff, offset.toInt(), length)
while (gocryptfsVolume.read_file(handleID, offset, ioBuffer).also { length = it } > 0){
System.arraycopy(ioBuffer, 0, fileBuff, offset.toInt(), length)
offset += length.toLong()
}
gocryptfsVolume.close_file(handleID)
@ -65,7 +59,7 @@ abstract class FileViewerActivity: BaseActivity() {
} catch (e: OutOfMemoryError){
ColoredAlertDialog(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.outofmemoryerror_msg))
.setMessage(R.string.outofmemoryerror_msg)
.setCancelable(false)
.setPositiveButton(getString(R.string.ok)) { _, _ -> finish() }
.show()
@ -81,30 +75,4 @@ abstract class FileViewerActivity: BaseActivity() {
}
return null
}
fun exportFile(path: String): Uri? {
val tmpFileUri = TemporaryFileProvider.createFile(this, File(path).name)
cachedFiles.add(tmpFileUri)
return if (gocryptfsVolume.export_file(this, path, tmpFileUri)) {
tmpFileUri
} else {
ColoredAlertDialog(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.export_failed, path))
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> finish() }
.show()
null
}
}
override fun onDestroy() {
super.onDestroy()
Thread{
for (uri in cachedFiles) {
if (Wiper.wipe(this, uri)){
cachedFiles.remove(uri)
}
}
}.start()
}
}

View File

@ -0,0 +1,53 @@
package sushi.hardcore.droidfs.file_viewers
import android.net.Uri
import com.google.android.exoplayer2.upstream.*
import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.util.GocryptfsVolume
class GocryptfsDataSource(private val gocryptfsVolume: GocryptfsVolume, private val filePath: String): DataSource {
private var handleID = -1
private var fileSize: Long = -1
private var fileOffset: Long = 0
override fun open(dataSpec: DataSpec?): Long {
dataSpec?.let {
fileOffset = dataSpec.position
}
handleID = gocryptfsVolume.open_read_mode(filePath)
fileSize = gocryptfsVolume.get_size(filePath)
return fileSize
}
override fun getUri(): Uri {
return ConstValues.fakeUri
}
override fun close() {
gocryptfsVolume.close_file(handleID)
}
override fun addTransferListener(transferListener: TransferListener?) {
//too lazy to implement this
}
override fun read(buffer: ByteArray, offset: Int, readLength: Int): Int {
if (fileOffset >= fileSize){
return -1
}
val tmpBuff = if (fileOffset+readLength > fileSize){
ByteArray((fileSize-fileOffset).toInt())
} else {
ByteArray(readLength)
}
val read = gocryptfsVolume.read_file(handleID, fileOffset, tmpBuff)
fileOffset += read
System.arraycopy(tmpBuff, 0, buffer, offset, read)
return read
}
class Factory(private val gocryptfsVolume: GocryptfsVolume, private val filePath: String): DataSource.Factory{
override fun createDataSource(): DataSource {
return GocryptfsDataSource(gocryptfsVolume, filePath)
}
}
}

View File

@ -0,0 +1,64 @@
package sushi.hardcore.droidfs.file_viewers
import androidx.appcompat.app.AlertDialog
import com.google.android.exoplayer2.ExoPlaybackException
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.source.LoopingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import sushi.hardcore.droidfs.ConstValues
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.widgets.ColoredAlertDialog
abstract class MediaPlayer: FileViewerActivity() {
private lateinit var player: SimpleExoPlayer
private var currentWindow = 0
private var playbackPosition: Long = 0
private lateinit var errorDialog: AlertDialog.Builder
override fun viewFile() {
errorDialog = ColoredAlertDialog(this)
.setTitle(R.string.error)
.setMessage(R.string.playing_failed)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> finish() }
}
abstract fun bindPlayer(player: SimpleExoPlayer)
private fun initializePlayer(){
player = SimpleExoPlayer.Builder(this).build()
bindPlayer(player)
val dataSourceFactory = GocryptfsDataSource.Factory(gocryptfsVolume, filePath)
val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(ConstValues.fakeUri)
player.seekTo(currentWindow, playbackPosition)
player.playWhenReady = true
player.addListener(object : Player.EventListener{
override fun onPlayerError(error: ExoPlaybackException) {
if (error.type == ExoPlaybackException.TYPE_SOURCE){
errorDialog.show()
}
}
})
player.prepare(LoopingMediaSource(mediaSource), false, false)
}
private fun releasePlayer(){
if (::player.isInitialized) {
playbackPosition = player.currentPosition
currentWindow = player.currentWindowIndex
player.release()
}
}
override fun onResume() {
super.onResume()
hideSystemUi()
initializePlayer()
}
override fun onPause() {
super.onPause()
releasePlayer()
}
}

View File

@ -21,8 +21,8 @@ class TextEditor: FileViewerActivity() {
private lateinit var titleText: TextView
private var changedSinceLastSave = false
private var wordWrap = true
override fun toggleFullscreen() {
//don't toggle fullscreen
override fun hideSystemUi() {
//don't hide system ui
}
override fun viewFile() {
loadWholeFile(filePath)?.let {
@ -32,7 +32,7 @@ class TextEditor: FileViewerActivity() {
} catch (e: OutOfMemoryError){
ColoredAlertDialog(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.outofmemoryerror_msg))
.setMessage(R.string.outofmemoryerror_msg)
.setCancelable(false)
.setPositiveButton(getString(R.string.ok)) { _, _ -> finish() }
.show()
@ -100,7 +100,7 @@ class TextEditor: FileViewerActivity() {
if (changedSinceLastSave){
ColoredAlertDialog(this)
.setTitle(R.string.warning)
.setMessage(getString(R.string.ask_save))
.setMessage(R.string.ask_save)
.setPositiveButton(getString(R.string.save)) { _, _ ->
if (save()){
finish()

View File

@ -1,27 +1,16 @@
package sushi.hardcore.droidfs.file_viewers
import android.widget.MediaController
import com.google.android.exoplayer2.SimpleExoPlayer
import kotlinx.android.synthetic.main.activity_video_player.*
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.widgets.ColoredAlertDialog
class VideoPlayer: FileViewerActivity() {
class VideoPlayer: MediaPlayer() {
override fun viewFile() {
val mc = MediaController(this)
super.viewFile()
setContentView(R.layout.activity_video_player)
mc.setAnchorView(video_player)
video_player.setOnErrorListener { _, _, _ ->
ColoredAlertDialog(this)
.setTitle(R.string.error)
.setMessage(getString(R.string.video_play_failed))
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> finish() }
.show()
true
}
val tmpFileUri = exportFile(filePath)
video_player.setVideoURI(tmpFileUri)
video_player.setMediaController(mc)
video_player.start()
}
override fun bindPlayer(player: SimpleExoPlayer) {
video_player.player = player
}
}

View File

@ -0,0 +1,114 @@
package sushi.hardcore.droidfs.provider
import android.content.ContentProvider
import android.content.ContentValues
import android.database.Cursor
import android.database.MatrixCursor
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.provider.MediaStore
import sushi.hardcore.droidfs.BuildConfig
import sushi.hardcore.droidfs.util.Wiper
import java.io.File
import java.util.*
import java.util.regex.Pattern
class RestrictedFileProvider: ContentProvider() {
companion object {
private const val AUTHORITY = BuildConfig.APPLICATION_ID + ".temporary_provider"
private val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY")
const val TEMPORARY_FILES_DIR_NAME = "temp"
private val UUID_PATTERN = Pattern.compile("[a-fA-F0-9-]+")
private lateinit var tempFilesDir: File
private val tempFiles = mutableMapOf<String, TemporaryFile>()
class TemporaryFile(val fileName: String, val file: File)
fun newFile(fileName: String): Uri? {
val uuid = UUID.randomUUID().toString()
val file = File(tempFilesDir, uuid)
return if (file.createNewFile()){
tempFiles[uuid] = TemporaryFile(fileName, file)
Uri.withAppendedPath(CONTENT_URI, uuid)
} else {
null
}
}
fun wipeAll() {
tempFilesDir.listFiles()?.let{
for (file in it) {
Wiper.wipe(file)
}
}
}
private fun getFileFromUri(uri: Uri): TemporaryFile? {
val uuid = uri.lastPathSegment
if (uuid != null) {
if (UUID_PATTERN.matcher(uuid).matches()) {
return tempFiles[uuid]
}
}
return null
}
}
override fun onCreate(): Boolean {
context?.let {
tempFilesDir = File(it.cacheDir, TEMPORARY_FILES_DIR_NAME)
return tempFilesDir.mkdirs()
}
return false
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
throw RuntimeException("Operation not supported")
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int {
throw RuntimeException("Operation not supported")
}
override fun query(uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
val temporaryFile = getFileFromUri(uri)
if (temporaryFile != null) {
val cursor = MatrixCursor(
arrayOf(
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.SIZE
)
)
cursor.newRow()
.add(temporaryFile.fileName)
.add(temporaryFile.file.length())
return cursor
}
return null
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
val temporaryFile = getFileFromUri(uri)
if (temporaryFile != null) {
Wiper.wipe(temporaryFile.file)
tempFiles.remove(uri.lastPathSegment)
}
return 1
}
override fun getType(uri: Uri): String {
return "application/octet-stream"
}
override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
if (("w" in mode && callingPackage == BuildConfig.APPLICATION_ID) || "w" !in mode) {
getFileFromUri(uri)?.let{
return ParcelFileDescriptor.open(it.file, ParcelFileDescriptor.parseMode(mode))
}
} else {
throw SecurityException("Read-only access")
}
return null
}
}

View File

@ -1,326 +0,0 @@
/*
* Copyright (C) 2017 Schürmann & Breitmoser GbR
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package sushi.hardcore.droidfs.provider;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.ClipDescription;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.util.Log;
import androidx.annotation.NonNull;
import sushi.hardcore.droidfs.BuildConfig;
import sushi.hardcore.droidfs.util.DatabaseUtil;
import sushi.hardcore.droidfs.util.Wiper;
/**
* Borrowed from OpenKeyChain
* I removed the scheduled cleanup because it requires unwanted permissions and doesn't work very well.
* But don't panic ! The "clear_cache" function from ExternalProvider do the same job when needed.
**/
/**
* TemporaryStorageProvider stores decrypted files inside the app's cache directory previously to
* sharing them with other applications.
* <p/>
* Security:
* - It is writable by OpenKeychain only (see Manifest), but exported for reading files
* - It uses UUIDs as identifiers which makes predicting files from outside impossible
* - Querying a number of files is not allowed, only querying single files
* -> You can only open a file if you know the Uri containing the precise UUID, this Uri is only
* revealed when the user shares a decrypted file with another app.
* <p/>
* Why is support lib's FileProvider not used?
* Because granting Uri permissions temporarily does not work correctly. See
* - https://code.google.com/p/android/issues/detail?id=76683
* - https://github.com/nmr8acme/FileProvider-permission-bug
* - http://stackoverflow.com/q/24467696
* - http://stackoverflow.com/q/18249007
* - Comments at http://www.blogc.at/2014/03/23/share-private-files-with-other-apps-fileprovider/
*/
public class TemporaryFileProvider extends ContentProvider {
private static final int TEMPFILE_TTL = 10 * 60 * 1000; // 10 minutes
private static final String DB_NAME = "tempstorage.db";
private static final String TABLE_FILES = "files";
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".tempstorage";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
private static final int DB_VERSION = 3;
interface TemporaryFileColumns {
String COLUMN_UUID = "id";
String COLUMN_NAME = "name";
String COLUMN_TIME = "time";
String COLUMN_TYPE = "mimetype";
}
private static final String TEMP_FILES_DIR = "temp";
private static File tempFilesDir;
private static Pattern UUID_PATTERN = Pattern.compile("[a-fA-F0-9-]+");
public static void wipeAll(){
for (File f: tempFilesDir.listFiles()){
Wiper.wipe(f);
}
}
public static Uri createFile(Context context, String targetName, String mimeType) {
ContentResolver contentResolver = context.getContentResolver();
ContentValues contentValues = new ContentValues();
contentValues.put(TemporaryFileColumns.COLUMN_NAME, targetName);
contentValues.put(TemporaryFileColumns.COLUMN_TYPE, mimeType);
contentValues.put(TemporaryFileColumns.COLUMN_TIME, System.currentTimeMillis());
Uri resultUri = contentResolver.insert(CONTENT_URI, contentValues);
//scheduleCleanupAfterTtl(context);
return resultUri;
}
public static Uri createFile(Context context, String targetName) {
return createFile(context, targetName, null);
}
public static Uri createFile(Context context) {
ContentValues contentValues = new ContentValues();
return context.getContentResolver().insert(CONTENT_URI, contentValues);
}
public static int setName(Context context, Uri uri, String name) {
ContentValues values = new ContentValues();
values.put(TemporaryFileColumns.COLUMN_NAME, name);
return context.getContentResolver().update(uri, values, null, null);
}
public static int setMimeType(Context context, Uri uri, String mimetype) {
ContentValues values = new ContentValues();
values.put(TemporaryFileColumns.COLUMN_TYPE, mimetype);
return context.getContentResolver().update(uri, values, null, null);
}
private static class TemporaryStorageDatabase extends SQLiteOpenHelper {
public TemporaryStorageDatabase(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_FILES + " (" +
TemporaryFileColumns.COLUMN_UUID + " TEXT PRIMARY KEY, " +
TemporaryFileColumns.COLUMN_NAME + " TEXT, " +
TemporaryFileColumns.COLUMN_TYPE + " TEXT, " +
TemporaryFileColumns.COLUMN_TIME + " INTEGER" +
");");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
switch (oldVersion) {
case 1:
db.execSQL("DROP TABLE IF EXISTS files");
db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_FILES + " (" +
TemporaryFileColumns.COLUMN_UUID + " TEXT PRIMARY KEY, " +
TemporaryFileColumns.COLUMN_NAME + " TEXT, " +
TemporaryFileColumns.COLUMN_TIME + " INTEGER" +
");");
case 2:
db.execSQL("ALTER TABLE files ADD COLUMN " + TemporaryFileColumns.COLUMN_TYPE + " TEXT");
}
}
}
private static TemporaryStorageDatabase db;
private File getFile(Uri uri) throws FileNotFoundException {
try {
return getFile(uri.getLastPathSegment());
} catch (NumberFormatException e) {
throw new FileNotFoundException();
}
}
private File getFile(String id) {
Matcher m = UUID_PATTERN.matcher(id);
if (!m.matches()) {
throw new SecurityException("Can only open temporary files with UUIDs!");
}
return new File(tempFilesDir, id);
}
@Override
public boolean onCreate() {
db = new TemporaryStorageDatabase(getContext());
tempFilesDir = new File(getContext().getCacheDir(), TEMP_FILES_DIR);
return tempFilesDir.mkdirs();
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
if (uri.getLastPathSegment() == null) {
throw new SecurityException("Listing temporary files is not allowed, only querying single files.");
}
File file;
try {
file = getFile(uri);
} catch (FileNotFoundException e) {
return null;
}
Cursor fileName = db.getReadableDatabase().query(TABLE_FILES,
new String[]{TemporaryFileColumns.COLUMN_NAME},
TemporaryFileColumns.COLUMN_UUID + "=?",
new String[]{uri.getLastPathSegment()}, null, null, null);
if (fileName != null) {
if (fileName.moveToNext()) {
MatrixCursor cursor = new MatrixCursor(new String[]{
MediaStore.MediaColumns.DISPLAY_NAME,
MediaStore.MediaColumns.SIZE,
MediaStore.MediaColumns.DATA,
});
cursor.newRow()
.add(fileName.getString(0))
.add(file.length())
.add(file.getAbsolutePath());
fileName.close();
return cursor;
}
fileName.close();
}
return null;
}
@Override
public String getType(Uri uri) {
Cursor cursor = db.getReadableDatabase().query(TABLE_FILES,
new String[]{TemporaryFileColumns.COLUMN_TYPE},
TemporaryFileColumns.COLUMN_UUID + "=?",
new String[]{uri.getLastPathSegment()}, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToNext()) {
if (!cursor.isNull(0)) {
return cursor.getString(0);
}
}
} finally {
cursor.close();
}
}
return "application/octet-stream";
}
@Override
public String[] getStreamTypes(Uri uri, String mimeTypeFilter) {
String type = getType(uri);
if (ClipDescription.compareMimeTypes(type, mimeTypeFilter)) {
return new String[]{type};
}
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
String uuid = UUID.randomUUID().toString();
values.put(TemporaryFileColumns.COLUMN_UUID, uuid);
int insert = (int) db.getWritableDatabase().insert(TABLE_FILES, null, values);
if (insert == -1) {
return null;
}
try {
getFile(uuid).createNewFile();
} catch (IOException e) {
return null;
}
return Uri.withAppendedPath(CONTENT_URI, uuid);
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
if (uri == null) {
return 0;
}
String fileUuidFromUri = uri.getLastPathSegment();
if (fileUuidFromUri != null) {
selection = DatabaseUtil.concatenateWhere(selection, TemporaryFileColumns.COLUMN_UUID + "=?");
selectionArgs = DatabaseUtil.appendSelectionArgs(selectionArgs, new String[]{ fileUuidFromUri });
}
Cursor files = db.getReadableDatabase().query(TABLE_FILES, new String[]{TemporaryFileColumns.COLUMN_UUID}, selection,
selectionArgs, null, null, null);
if (files != null) {
while (files.moveToNext()) {
getFile(files.getString(0)).delete();
}
files.close();
return db.getWritableDatabase().delete(TABLE_FILES, selection, selectionArgs);
}
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
if (values.size() != 1) {
throw new UnsupportedOperationException("Update supported only for one field at a time!");
}
if (!values.containsKey(TemporaryFileColumns.COLUMN_NAME) && !values.containsKey(TemporaryFileColumns.COLUMN_TYPE)) {
throw new UnsupportedOperationException("Update supported only for name and type field!");
}
if (selection != null || selectionArgs != null) {
throw new UnsupportedOperationException("Update supported only for plain uri!");
}
return db.getWritableDatabase().update(TABLE_FILES, values,
TemporaryFileColumns.COLUMN_UUID + " = ?", new String[]{uri.getLastPathSegment()});
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
return openFileHelper(uri, mode);
}
}

View File

@ -1,84 +0,0 @@
/*
* Copyright (C) 2017 Schürmann & Breitmoser GbR
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package sushi.hardcore.droidfs.util;
import androidx.sqlite.db.SupportSQLiteDatabase;
import android.database.Cursor;
import android.text.TextUtils;
/**
* Borrowed from OpenKeyChain
*/
/**
* Shamelessly copied from android.database.DatabaseUtils
*/
public class DatabaseUtil {
/**
* Concatenates two SQL WHERE clauses, handling empty or null values.
*/
public static String concatenateWhere(String a, String b) {
if (TextUtils.isEmpty(a)) {
return b;
}
if (TextUtils.isEmpty(b)) {
return a;
}
return "(" + a + ") AND (" + b + ")";
}
/**
* Appends one set of selection args to another. This is useful when adding a selection
* argument to a user provided set.
*/
public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) {
if (originalValues == null || originalValues.length == 0) {
return newValues;
}
String[] result = new String[originalValues.length + newValues.length ];
System.arraycopy(originalValues, 0, result, 0, originalValues.length);
System.arraycopy(newValues, 0, result, originalValues.length, newValues.length);
return result;
}
public static void explainQuery(SupportSQLiteDatabase db, String sql) {
Cursor explainCursor = db.query("EXPLAIN QUERY PLAN " + sql, new String[0]);
// this is a debugging feature, we can be a little careless
explainCursor.moveToFirst();
StringBuilder line = new StringBuilder();
for (int i = 0; i < explainCursor.getColumnCount(); i++) {
line.append(explainCursor.getColumnName(i)).append(", ");
}
while (!explainCursor.isAfterLast()) {
line = new StringBuilder();
for (int i = 0; i < explainCursor.getColumnCount(); i++) {
line.append(explainCursor.getString(i)).append(", ");
}
explainCursor.moveToNext();
}
explainCursor.close();
}
}

View File

@ -3,9 +3,8 @@ package sushi.hardcore.droidfs.util
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.appcompat.app.AlertDialog
import sushi.hardcore.droidfs.R
import sushi.hardcore.droidfs.provider.TemporaryFileProvider
import sushi.hardcore.droidfs.provider.RestrictedFileProvider
import sushi.hardcore.droidfs.widgets.ColoredAlertDialog
import java.io.File
import java.net.URLConnection
@ -13,51 +12,53 @@ import java.util.*
object ExternalProvider {
private const val content_type_all = "*/*"
private var cached_files: MutableList<Uri> = ArrayList()
private fun get_content_type(filename: String, previous_content_type: String?): String? {
private var storedFiles: MutableList<Uri> = ArrayList()
private fun getContentType(filename: String, previous_content_type: String?): String? {
if (content_type_all != previous_content_type) {
var content_type = URLConnection.guessContentTypeFromName(filename)
if (content_type == null) {
content_type = content_type_all
var contentType = URLConnection.guessContentTypeFromName(filename)
if (contentType == null) {
contentType = content_type_all
}
if (previous_content_type == null) {
return content_type
} else if (previous_content_type != content_type) {
return contentType
} else if (previous_content_type != contentType) {
return content_type_all
}
}
return previous_content_type
}
private fun export_file(context: Context, gocryptfsVolume: GocryptfsVolume, file_path: String, previous_content_type: String?): Export_file_result {
val filename = File(file_path).name
val tmp_file_uri = TemporaryFileProvider.createFile(context, filename)
cached_files.add(tmp_file_uri)
if (gocryptfsVolume.export_file(context, file_path, tmp_file_uri)) {
return Export_file_result(tmp_file_uri, get_content_type(filename, previous_content_type))
private fun exportFile(context: Context, gocryptfsVolume: GocryptfsVolume, file_path: String, previous_content_type: String?): Pair<Uri?, String?> {
val fileName = File(file_path).name
val tmpFileUri = RestrictedFileProvider.newFile(fileName)
if (tmpFileUri != null){
storedFiles.add(tmpFileUri)
if (gocryptfsVolume.export_file(context, file_path, tmpFileUri)) {
return Pair(tmpFileUri, getContentType(fileName, previous_content_type))
}
}
ColoredAlertDialog(context)
.setTitle(R.string.error)
.setMessage(context.getString(R.string.export_failed, file_path))
.setPositiveButton(R.string.ok, null)
.show()
return Export_file_result(null, null)
return Pair(null, null)
}
fun share(context: Context, gocryptfsVolume: GocryptfsVolume, file_paths: List<String>) {
var content_type: String? = null
var contentType: String? = null
val uris = ArrayList<Uri>()
for (path in file_paths) {
val result = export_file(context, gocryptfsVolume, path, content_type)
content_type = if (result.uri == null) {
return
val result = exportFile(context, gocryptfsVolume, path, contentType)
contentType = if (result.first != null) {
uris.add(result.first!!)
result.second
} else {
uris.add(result.uri!!)
result.content_type
return
}
}
val shareIntent = Intent()
shareIntent.type = content_type
shareIntent.type = contentType
if (uris.size == 1) {
shareIntent.action = Intent.ACTION_SEND
shareIntent.putExtra(Intent.EXTRA_STREAM, uris[0])
@ -69,24 +70,22 @@ object ExternalProvider {
}
fun open(context: Context, gocryptfsVolume: GocryptfsVolume, file_path: String) {
val result = export_file(context, gocryptfsVolume, file_path, null)
result.uri?.let {
val result = exportFile(context, gocryptfsVolume, file_path, null)
result.first?.let {
val openIntent = Intent()
openIntent.action = Intent.ACTION_VIEW
openIntent.setDataAndType(result.uri, result.content_type)
openIntent.setDataAndType(result.first, result.second)
context.startActivity(openIntent)
}
}
fun clear_cache(context: Context) {
fun removeFiles(context: Context) {
Thread{
for (uri in cached_files) {
for (uri in storedFiles) {
if (Wiper.wipe(context, uri)){
cached_files.remove(uri)
storedFiles.remove(uri)
}
}
}.start()
}
private class Export_file_result(var uri: Uri?, var content_type: String?)
}

View File

@ -26,7 +26,6 @@ class GocryptfsVolume(var sessionID: Int) {
const val KeyLen = 32
const val ScryptDefaultLogN = 16
const val DefaultBS = 4096
//external fun scrypt_hash(data: CharArray, logN: Int): ByteArray
external fun create_volume(root_cipher_dir: String, password: CharArray, logN: Int, creator: String): Boolean
external fun init(root_cipher_dir: String, password: CharArray?, givenHash: ByteArray?, returnedHash: ByteArray?): Int
external fun change_password(root_cipher_dir: String, old_password: CharArray?, givenHash: ByteArray?, new_password: CharArray, returnedHash: ByteArray?): Boolean

View File

@ -1,42 +1,18 @@
package sushi.hardcore.droidfs.util;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.storage.StorageManager;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import androidx.annotation.Nullable;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.text.DecimalFormat;
import java.util.ArrayList;
public final class FilesUtils {
/*public static ArrayList<File> recursive_get_files(File root_path){
ArrayList<File> results_files = new ArrayList<>();
final File[] elements = root_path.listFiles();
if (elements != null){
for (File item : elements){
if (item.isDirectory()){
results_files.addAll(recursive_get_files(item));
} else if (item.isFile()){
results_files.add(item);
}
}
}
return results_files;
}*/
public class PathUtils {
public static String get_parent_path(String path){
if (path.endsWith("/")){

View File

@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:gravity="center_vertical"
android:background="@color/fullScreenBackgroundColor"
android:orientation="vertical">
@ -14,33 +15,11 @@
android:textSize="18sp"
android:padding="10dp"/>
<sushi.hardcore.droidfs.widgets.ColoredSeekBar
android:id="@+id/seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<LinearLayout
android:layout_width="match_parent"
<com.google.android.exoplayer2.ui.PlayerControlView
android:id="@+id/audio_controller"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_horizontal">
<sushi.hardcore.droidfs.widgets.ColoredImageButton
android:id="@+id/button_pause"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/icon_pause"
android:scaleType="fitCenter"
android:background="#00000000"/>
<sushi.hardcore.droidfs.widgets.ColoredImageButton
android:id="@+id/button_stop"
android:layout_width="50dp"
android:layout_height="50dp"
android:src="@drawable/icon_stop"
android:scaleType="fitCenter"
android:background="#00000000"/>
</LinearLayout>
app:controller_layout_id="@layout/exo_custom_playback_control"
app:show_timeout="0"/>
</LinearLayout>

View File

@ -2,13 +2,15 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:gravity="center"
android:background="@color/fullScreenBackgroundColor">
<VideoView
<com.google.android.exoplayer2.ui.SimpleExoPlayerView
android:id="@+id/video_player"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"/>
android:layout_gravity="center"
app:controller_layout_id="@layout/exo_custom_playback_control"/>
</LinearLayout>

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layoutDirection="ltr"
android:background="#CC000000"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="4dp"
android:orientation="horizontal">
<ImageButton android:id="@id/exo_rew"
style="@style/ExoMediaButton.Rewind"/>
<ImageButton android:id="@id/exo_shuffle"
style="@style/ExoMediaButton"/>
<ImageButton android:id="@id/exo_repeat_toggle"
style="@style/ExoMediaButton"/>
<ImageButton android:id="@id/exo_play"
style="@style/ExoMediaButton.Play"/>
<ImageButton android:id="@id/exo_pause"
style="@style/ExoMediaButton.Pause"/>
<ImageButton android:id="@id/exo_ffwd"
style="@style/ExoMediaButton.FastForward"/>
<ImageButton android:id="@id/exo_vr"
style="@style/ExoMediaButton.VR"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView android:id="@id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:includeFontPadding="false"
android:textColor="#FFBEBEBE"/>
<View android:id="@id/exo_progress_placeholder"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="26dp"/>
<TextView android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:includeFontPadding="false"
android:textColor="#FFBEBEBE"/>
</LinearLayout>
</LinearLayout>

View File

@ -109,7 +109,7 @@
<string name="video">Video</string>
<string name="audio">Audio</string>
<string name="media_player_prepare_failed">Failed to initialize the media player.</string>
<string name="video_play_failed">Failed to play this video.</string>
<string name="playing_failed">Failed to play this file.</string>
<string name="text">Text</string>
<string name="save_failed">Save failed</string>
<string name="file_saved">File saved !</string>

View File

@ -2,11 +2,8 @@
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.Cyanea.Dark.NoActionBar">
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<!--<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorAccent">@color/colorAccent</item>-->
<item name="android:colorBackground">@color/backgroundColor</item>
<item name="android:textColor">@color/textColor</item>
<!--<item name="buttonStyle">@style/button</item>-->
<item name="checkboxStyle">@style/checkbox</item>
<!--<item name="actionOverflowMenuStyle">@style/menu_overflow</item>-->
</style>