Volume on SD cards warning

This commit is contained in:
Hardcore Sushi 2020-08-25 14:10:46 +02:00
parent 29b12898a4
commit f44a702647
9 changed files with 285 additions and 151 deletions

View File

@ -20,6 +20,7 @@ import sushi.hardcore.droidfs.adapters.SavedVolumesAdapter
import sushi.hardcore.droidfs.fingerprint_stuff.FingerprintPasswordHashSaver import sushi.hardcore.droidfs.fingerprint_stuff.FingerprintPasswordHashSaver
import sushi.hardcore.droidfs.util.* import sushi.hardcore.droidfs.util.*
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
import java.io.File
import java.util.* import java.util.*
class ChangePasswordActivity : BaseActivity() { class ChangePasswordActivity : BaseActivity() {
@ -80,9 +81,25 @@ class ChangePasswordActivity : BaseActivity() {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
if (requestCode == PICK_DIRECTORY_REQUEST_CODE) { if (requestCode == PICK_DIRECTORY_REQUEST_CODE) {
if (data != null) { if (data?.data != null) {
if (PathUtils.isTreeUriOnPrimaryStorage(data.data)){
val path = PathUtils.getFullPathFromTreeUri(data.data, this) val path = PathUtils.getFullPathFromTreeUri(data.data, this)
if (path != null){
edit_volume_path.setText(path) edit_volume_path.setText(path)
} else {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(R.string.path_from_uri_null_error_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
} else {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.warning)
.setMessage(R.string.change_pwd_on_sdcard_error_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
} }
} }
} }
@ -92,38 +109,53 @@ class ChangePasswordActivity : BaseActivity() {
rootCipherDir = edit_volume_path.text.toString() rootCipherDir = edit_volume_path.text.toString()
if (rootCipherDir.isEmpty()) { if (rootCipherDir.isEmpty()) {
Toast.makeText(this, R.string.enter_volume_path, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.enter_volume_path, Toast.LENGTH_SHORT).show()
} else {
if (!File(rootCipherDir).canWrite()){
ColoredAlertDialogBuilder(this)
.setTitle(R.string.warning)
.setMessage(R.string.change_pwd_cant_write_error_msg)
.setPositiveButton(R.string.ok, null)
.show()
} else { } else {
changePassword(null) changePassword(null)
} }
} }
}
private fun changePassword(givenHash: ByteArray?){ private fun changePassword(givenHash: ByteArray?){
object : LoadingTask(this, R.string.loading_msg_change_password){
override fun doTask(activity: AppCompatActivity) {
val newPassword = edit_new_password.text.toString().toCharArray() val newPassword = edit_new_password.text.toString().toCharArray()
val newPasswordConfirm = edit_new_password_confirm.text.toString().toCharArray() val newPasswordConfirm = edit_new_password_confirm.text.toString().toCharArray()
if (!newPassword.contentEquals(newPasswordConfirm)) { if (!newPassword.contentEquals(newPasswordConfirm)) {
stopTaskWithToast(R.string.passwords_mismatch) Toast.makeText(this, R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
} else { } else {
object : LoadingTask(this, R.string.loading_msg_change_password) {
override fun doTask(activity: AppCompatActivity) {
val oldPassword = edit_old_password.text.toString().toCharArray() val oldPassword = edit_old_password.text.toString().toCharArray()
var returnedHash: ByteArray? = null var returnedHash: ByteArray? = null
if (usf_fingerprint && checkbox_save_password.isChecked){ if (usf_fingerprint && checkbox_save_password.isChecked) {
returnedHash = ByteArray(GocryptfsVolume.KeyLen) returnedHash = ByteArray(GocryptfsVolume.KeyLen)
} }
var changePasswordImmediately = true var changePasswordImmediately = true
if (givenHash == null){ if (givenHash == null) {
val cipherText = sharedPrefs.getString(rootCipherDir, null) val cipherText = sharedPrefs.getString(rootCipherDir, null)
if (cipherText != null){ //password hash saved if (cipherText != null) { //password hash saved
stopTask { stopTask {
fingerprintPasswordHashSaver.decrypt(cipherText, rootCipherDir, ::changePassword) fingerprintPasswordHashSaver.decrypt(cipherText, rootCipherDir, ::changePassword)
} }
changePasswordImmediately = false changePasswordImmediately = false
} }
} }
if (changePasswordImmediately){ if (changePasswordImmediately) {
if (GocryptfsVolume.changePassword(rootCipherDir, oldPassword, givenHash, newPassword, returnedHash)) { if (GocryptfsVolume.changePassword(
rootCipherDir,
oldPassword,
givenHash,
newPassword,
returnedHash
)
) {
val editor = sharedPrefs.edit() val editor = sharedPrefs.edit()
if (sharedPrefs.getString(rootCipherDir, null) != null){ if (sharedPrefs.getString(rootCipherDir, null) != null) {
editor.remove(rootCipherDir) editor.remove(rootCipherDir)
editor.apply() editor.apply()
} }
@ -133,17 +165,20 @@ class ChangePasswordActivity : BaseActivity() {
val newSavedVolumesPaths = oldSavedVolumesPaths.toMutableList() val newSavedVolumesPaths = oldSavedVolumesPaths.toMutableList()
if (!oldSavedVolumesPaths.contains(rootCipherDir)) { if (!oldSavedVolumesPaths.contains(rootCipherDir)) {
newSavedVolumesPaths.add(rootCipherDir) newSavedVolumesPaths.add(rootCipherDir)
editor.putStringSet(ConstValues.saved_volumes_key, newSavedVolumesPaths.toSet()) editor.putStringSet(
ConstValues.saved_volumes_key,
newSavedVolumesPaths.toSet()
)
editor.apply() editor.apply()
} }
if (checkbox_save_password.isChecked && returnedHash != null){ if (checkbox_save_password.isChecked && returnedHash != null) {
fingerprintPasswordHashSaver.encryptAndSave(returnedHash, rootCipherDir){ _ -> fingerprintPasswordHashSaver.encryptAndSave(returnedHash, rootCipherDir) { _ ->
stopTask { onPasswordChanged() } stopTask { onPasswordChanged() }
} }
continueImmediately = false continueImmediately = false
} }
} }
if (continueImmediately){ if (continueImmediately) {
stopTask { onPasswordChanged() } stopTask { onPasswordChanged() }
} }
} else { } else {
@ -158,11 +193,13 @@ class ChangePasswordActivity : BaseActivity() {
} }
Arrays.fill(oldPassword, 0.toChar()) Arrays.fill(oldPassword, 0.toChar())
} }
override fun doFinally(activity: AppCompatActivity) {
Arrays.fill(newPassword, 0.toChar()) Arrays.fill(newPassword, 0.toChar())
Arrays.fill(newPasswordConfirm, 0.toChar()) Arrays.fill(newPasswordConfirm, 0.toChar())
} }
} }
} }
}
private fun onPasswordChanged(){ private fun onPasswordChanged(){
ColoredAlertDialogBuilder(this) ColoredAlertDialogBuilder(this)

View File

@ -5,12 +5,9 @@ import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_create.* import kotlinx.android.synthetic.main.activity_create.*
import kotlinx.android.synthetic.main.activity_create.checkbox_remember_path
import kotlinx.android.synthetic.main.activity_create.checkbox_save_password
import kotlinx.android.synthetic.main.activity_create.edit_password
import kotlinx.android.synthetic.main.activity_create.edit_volume_path
import kotlinx.android.synthetic.main.toolbar.* import kotlinx.android.synthetic.main.toolbar.*
import sushi.hardcore.droidfs.explorers.ExplorerActivity import sushi.hardcore.droidfs.explorers.ExplorerActivity
import sushi.hardcore.droidfs.fingerprint_stuff.FingerprintPasswordHashSaver import sushi.hardcore.droidfs.fingerprint_stuff.FingerprintPasswordHashSaver
@ -19,6 +16,7 @@ import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
import java.io.File import java.io.File
import java.util.* import java.util.*
class CreateActivity : BaseActivity() { class CreateActivity : BaseActivity() {
companion object { companion object {
private const val PICK_DIRECTORY_REQUEST_CODE = 1 private const val PICK_DIRECTORY_REQUEST_CODE = 1
@ -53,36 +51,71 @@ class CreateActivity : BaseActivity() {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
if (requestCode == PICK_DIRECTORY_REQUEST_CODE) { if (requestCode == PICK_DIRECTORY_REQUEST_CODE) {
if (data != null) { if (data?.data != null) {
if (PathUtils.isTreeUriOnPrimaryStorage(data.data)){
val path = PathUtils.getFullPathFromTreeUri(data.data, this) val path = PathUtils.getFullPathFromTreeUri(data.data, this)
if (path != null){
edit_volume_path.setText(path) edit_volume_path.setText(path)
} else {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(R.string.path_from_uri_null_error_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
} else {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.warning)
.setMessage(R.string.create_on_sdcard_error_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
} }
} }
} }
} }
fun onClickCreate(view: View?) { fun onClickCreate(view: View?) {
object: LoadingTask(this, R.string.loading_msg_create){ rootCipherDir = edit_volume_path.text.toString()
override fun doTask(activity: AppCompatActivity) { if (rootCipherDir.isEmpty()) {
Toast.makeText(this, R.string.enter_volume_path, Toast.LENGTH_SHORT).show()
} else {
val password = edit_password.text.toString().toCharArray() val password = edit_password.text.toString().toCharArray()
val passwordConfirm = edit_password_confirm.text.toString().toCharArray() val passwordConfirm = edit_password_confirm.text.toString().toCharArray()
if (!password.contentEquals(passwordConfirm)) { if (!password.contentEquals(passwordConfirm)) {
stopTaskWithToast(R.string.passwords_mismatch) Toast.makeText(this, R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
} else { } else {
rootCipherDir = edit_volume_path.text.toString() object: LoadingTask(this, R.string.loading_msg_create){
override fun doTask(activity: AppCompatActivity) {
val volumePathFile = File(rootCipherDir) val volumePathFile = File(rootCipherDir)
var goodDirectory = false var goodDirectory = false
if (!volumePathFile.isDirectory) { if (!volumePathFile.isDirectory) {
if (volumePathFile.mkdirs()) { if (volumePathFile.mkdirs()) {
goodDirectory = true goodDirectory = true
} else { } else {
stopTaskWithToast(R.string.error_mkdir) stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.warning)
.setMessage(R.string.create_cant_write_error_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
} }
} else { } else {
val dirContent = volumePathFile.list() val dirContent = volumePathFile.list()
if (dirContent != null){ if (dirContent != null){
if (dirContent.isEmpty()) { if (dirContent.isEmpty()) {
if (volumePathFile.canWrite()){
goodDirectory = true goodDirectory = true
} else {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.warning)
.setMessage(R.string.create_cant_write_error_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
}
} else { } else {
stopTaskWithToast(R.string.dir_not_empty) stopTaskWithToast(R.string.dir_not_empty)
} }
@ -136,11 +169,14 @@ class CreateActivity : BaseActivity() {
} }
} }
} }
override fun doFinally(activity: AppCompatActivity) {
Arrays.fill(password, 0.toChar()) Arrays.fill(password, 0.toChar())
Arrays.fill(passwordConfirm, 0.toChar()) Arrays.fill(passwordConfirm, 0.toChar())
} }
} }
} }
}
}
private fun startExplorer(){ private fun startExplorer(){
ColoredAlertDialogBuilder(this) ColoredAlertDialogBuilder(this)

View File

@ -7,8 +7,16 @@ import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.widget.AdapterView.OnItemClickListener import android.widget.AdapterView.OnItemClickListener
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_change_password.*
import kotlinx.android.synthetic.main.activity_create.*
import kotlinx.android.synthetic.main.activity_open.* import kotlinx.android.synthetic.main.activity_open.*
import kotlinx.android.synthetic.main.activity_open.checkbox_remember_path
import kotlinx.android.synthetic.main.activity_open.checkbox_save_password
import kotlinx.android.synthetic.main.activity_open.edit_password
import kotlinx.android.synthetic.main.activity_open.edit_volume_path
import kotlinx.android.synthetic.main.activity_open.saved_path_listview
import kotlinx.android.synthetic.main.toolbar.* import kotlinx.android.synthetic.main.toolbar.*
import sushi.hardcore.droidfs.adapters.SavedVolumesAdapter import sushi.hardcore.droidfs.adapters.SavedVolumesAdapter
import sushi.hardcore.droidfs.explorers.ExplorerActivity import sushi.hardcore.droidfs.explorers.ExplorerActivity
@ -82,21 +90,51 @@ class OpenActivity : BaseActivity() {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {
if (requestCode == PICK_DIRECTORY_REQUEST_CODE) { if (requestCode == PICK_DIRECTORY_REQUEST_CODE) {
if (data != null) { if (data?.data != null) {
if (PathUtils.isTreeUriOnPrimaryStorage(data.data)){
val path = PathUtils.getFullPathFromTreeUri(data.data, this) val path = PathUtils.getFullPathFromTreeUri(data.data, this)
if (path != null){
edit_volume_path.setText(path) edit_volume_path.setText(path)
} else {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(R.string.path_from_uri_null_error_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
} else {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.warning)
.setMessage(R.string.open_on_sdcard_warning)
.setPositiveButton(R.string.ok, null)
.show()
}
} }
} }
} }
} }
fun onClickOpen(view: View?) { fun onClickOpen(view: View?) {
rootCipherDir = edit_volume_path.text.toString()
if (rootCipherDir.isEmpty()) {
Toast.makeText(this, R.string.enter_volume_path, Toast.LENGTH_SHORT).show()
} else {
if (!File(rootCipherDir).canWrite()){
ColoredAlertDialogBuilder(this)
.setTitle(R.string.warning)
.setMessage(R.string.open_cant_write_warning)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> openVolume() }
.show()
} else {
openVolume()
}
}
}
private fun openVolume(){
object : LoadingTask(this, R.string.loading_msg_open){ object : LoadingTask(this, R.string.loading_msg_open){
override fun doTask(activity: AppCompatActivity) { override fun doTask(activity: AppCompatActivity) {
rootCipherDir = edit_volume_path.text.toString() //fresh get in case of manual rewrite
if (rootCipherDir.isEmpty()) {
stopTaskWithToast(R.string.enter_volume_path)
} else {
val password = edit_password.text.toString().toCharArray() val password = edit_password.text.toString().toCharArray()
var returnedHash: ByteArray? = null var returnedHash: ByteArray? = null
if (usf_fingerprint && checkbox_save_password.isChecked){ if (usf_fingerprint && checkbox_save_password.isChecked){
@ -130,7 +168,6 @@ class OpenActivity : BaseActivity() {
} }
} }
} }
}
private fun openUsingPasswordHash(passwordHash: ByteArray){ private fun openUsingPasswordHash(passwordHash: ByteArray){
object : LoadingTask(this, R.string.loading_msg_open){ object : LoadingTask(this, R.string.loading_msg_open){

View File

@ -194,7 +194,9 @@ open class BaseExplorerActivity : BaseActivity() {
} }
} }
total_size_text.text = getString(R.string.total_size, PathUtils.formatSize(totalSize)) total_size_text.text = getString(R.string.total_size, PathUtils.formatSize(totalSize))
runOnUiThread {
explorerAdapter.notifyDataSetChanged() explorerAdapter.notifyDataSetChanged()
}
}.start() }.start()
} }

View File

@ -6,7 +6,9 @@ import android.net.Uri;
import android.os.storage.StorageManager; import android.os.storage.StorageManager;
import android.provider.DocumentsContract; import android.provider.DocumentsContract;
import android.provider.OpenableColumns; import android.provider.OpenableColumns;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.io.File; import java.io.File;
import java.lang.reflect.Array; import java.lang.reflect.Array;
import java.lang.reflect.Method; import java.lang.reflect.Method;
@ -81,24 +83,35 @@ public class PathUtils {
return new DecimalFormat("#,##0.#").format(size/Math.pow(1024, digitGroups))+" "+units[digitGroups]; return new DecimalFormat("#,##0.#").format(size/Math.pow(1024, digitGroups))+" "+units[digitGroups];
} }
public static Boolean isTreeUriOnPrimaryStorage(Uri treeUri){
String volumeId = getVolumeIdFromTreeUri(treeUri);
if (volumeId != null) {
return volumeId.equals(PRIMARY_VOLUME_NAME) || volumeId.equals("home") || volumeId.equals("downloads");
} else {
return false;
}
}
private static final String PRIMARY_VOLUME_NAME = "primary"; private static final String PRIMARY_VOLUME_NAME = "primary";
@Nullable @Nullable
public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) { public static String getFullPathFromTreeUri(@Nullable Uri treeUri, Context context) {
if (treeUri == null) return null; if (treeUri == null) return null;
String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri),con); if ("content".equalsIgnoreCase(treeUri.getScheme())) {
if (volumePath == null) return File.separator; String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri),context);
if (volumePath == null) return null;
if (volumePath.endsWith(File.separator)) if (volumePath.endsWith(File.separator))
volumePath = volumePath.substring(0, volumePath.length() - 1); volumePath = volumePath.substring(0, volumePath.length() - 1);
String documentPath = getDocumentPathFromTreeUri(treeUri); String documentPath = getDocumentPathFromTreeUri(treeUri);
if (documentPath.endsWith(File.separator)) if (documentPath.endsWith(File.separator))
documentPath = documentPath.substring(0, documentPath.length() - 1); documentPath = documentPath.substring(0, documentPath.length() - 1);
if (documentPath.length() > 0) { if (documentPath.length() > 0) {
if (documentPath.startsWith(File.separator)) return path_join(volumePath, documentPath);
return volumePath + documentPath;
else
return volumePath + File.separator + documentPath;
} }
else return volumePath; else return volumePath;
} else if ("file".equalsIgnoreCase(treeUri.getScheme())) {
return treeUri.getPath();
}
return null;
} }
private static String getVolumePath(final String volumeId, Context context) { private static String getVolumePath(final String volumeId, Context context) {
@ -117,7 +130,6 @@ public class PathUtils {
Object storageVolumeElement = Array.get(result, i); Object storageVolumeElement = Array.get(result, i);
String uuid = (String) getUuid.invoke(storageVolumeElement); String uuid = (String) getUuid.invoke(storageVolumeElement);
Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement); Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) if (primary && PRIMARY_VOLUME_NAME.equals(volumeId))
return (String) getPath.invoke(storageVolumeElement); return (String) getPath.invoke(storageVolumeElement);
if (uuid != null && uuid.equals(volumeId)) if (uuid != null && uuid.equals(volumeId))

View File

@ -143,7 +143,7 @@
android:layout_marginHorizontal="@dimen/action_activity_button_horizontal_margin" android:layout_marginHorizontal="@dimen/action_activity_button_horizontal_margin"
android:layout_marginBottom="@dimen/action_activity_button_margin_bottom" android:layout_marginBottom="@dimen/action_activity_button_margin_bottom"
android:onClick="onClickChangePassword" android:onClick="onClickChangePassword"
android:text="@string/change_volume_password" android:text="@string/change_password"
style="@style/button"/> style="@style/button"/>
</LinearLayout> </LinearLayout>

View File

@ -114,7 +114,7 @@
android:layout_height="@dimen/action_activity_button_height" android:layout_height="@dimen/action_activity_button_height"
android:layout_marginHorizontal="@dimen/action_activity_button_horizontal_margin" android:layout_marginHorizontal="@dimen/action_activity_button_horizontal_margin"
android:onClick="onClickCreate" android:onClick="onClickCreate"
android:text="@string/create_volume" android:text="@string/create"
style="@style/button"/> style="@style/button"/>
</LinearLayout> </LinearLayout>

View File

@ -102,7 +102,7 @@
android:layout_marginHorizontal="@dimen/action_activity_button_horizontal_margin" android:layout_marginHorizontal="@dimen/action_activity_button_horizontal_margin"
android:layout_marginBottom="@dimen/action_activity_button_margin_bottom" android:layout_marginBottom="@dimen/action_activity_button_margin_bottom"
android:onClick="onClickOpen" android:onClick="onClickOpen"
android:text="@string/open_volume" android:text="@string/open"
style="@style/button"/> style="@style/button"/>
</LinearLayout> </LinearLayout>

View File

@ -1,8 +1,11 @@
<resources> <resources>
<string name="app_name">DroidFS</string> <string name="app_name">DroidFS</string>
<string name="open_volume">OPEN A VOLUME</string> <string name="open_volume">Open a volume</string>
<string name="create_volume">CREATE A VOLUME</string> <string name="create_volume">Create a volume</string>
<string name="change_volume_password">CHANGE VOLUME PASSWORD</string> <string name="change_volume_password">Change a volume\'s password</string>
<string name="open">Open</string>
<string name="create">Create</string>
<string name="change_password">Change password</string>
<string name="password">Password:</string> <string name="password">Password:</string>
<string name="password_confirm">Password (confirmation):</string> <string name="password_confirm">Password (confirmation):</string>
<string name="volume_path">Volume Path:</string> <string name="volume_path">Volume Path:</string>
@ -163,4 +166,11 @@
<string name="choose_filter">Choose filter</string> <string name="choose_filter">Choose filter</string>
<string name="filters_warning">Filters can only be applied to reduced quality images. If you want to take high definition photos, do not apply any filters.</string> <string name="filters_warning">Filters can only be applied to reduced quality images. If you want to take high definition photos, do not apply any filters.</string>
<string name="timer_empty_error_msg">Please enter a numeric value</string> <string name="timer_empty_error_msg">Please enter a numeric value</string>
<string name="path_from_uri_null_error_msg">Failed to retrieve the selected path.</string>
<string name="create_cant_write_error_msg">DroidFS doesn\'t have write access to this path. Please try another location.</string>
<string name="create_on_sdcard_error_msg">DroidFS can\'t write on removable SD cards, please select a path on internal storage.</string>
<string name="open_on_sdcard_warning">DroidFS can\'t write on removable SD cards. You will only have read-only access to the volumes therein.</string>
<string name="open_cant_write_warning">DroidFS doesn\'t have write access to this path. You will only have read-only access to this volume.</string>
<string name="change_pwd_cant_write_error_msg">DroidFS doesn\'t have write access to this path. You can try to move the volume to a writable location.</string>
<string name="change_pwd_on_sdcard_error_msg">DroidFS can\'t write on removable SD cards, please move the volume to internal storage.</string>
</resources> </resources>