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

View File

@ -5,12 +5,9 @@ import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
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 sushi.hardcore.droidfs.explorers.ExplorerActivity
import sushi.hardcore.droidfs.fingerprint_stuff.FingerprintPasswordHashSaver
@ -19,6 +16,7 @@ import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
import java.io.File
import java.util.*
class CreateActivity : BaseActivity() {
companion object {
private const val PICK_DIRECTORY_REQUEST_CODE = 1
@ -53,91 +51,129 @@ class CreateActivity : BaseActivity() {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
if (requestCode == PICK_DIRECTORY_REQUEST_CODE) {
if (data != null) {
val path = PathUtils.getFullPathFromTreeUri(data.data, this)
edit_volume_path.setText(path)
if (data?.data != null) {
if (PathUtils.isTreeUriOnPrimaryStorage(data.data)){
val path = PathUtils.getFullPathFromTreeUri(data.data, this)
if (path != null){
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?) {
object: LoadingTask(this, R.string.loading_msg_create){
override fun doTask(activity: AppCompatActivity) {
val password = edit_password.text.toString().toCharArray()
val passwordConfirm = edit_password_confirm.text.toString().toCharArray()
if (!password.contentEquals(passwordConfirm)) {
stopTaskWithToast(R.string.passwords_mismatch)
} else {
rootCipherDir = edit_volume_path.text.toString()
val volumePathFile = File(rootCipherDir)
var goodDirectory = false
if (!volumePathFile.isDirectory) {
if (volumePathFile.mkdirs()) {
goodDirectory = true
} else {
stopTaskWithToast(R.string.error_mkdir)
}
} else {
val dirContent = volumePathFile.list()
if (dirContent != null){
if (dirContent.isEmpty()) {
rootCipherDir = edit_volume_path.text.toString()
if (rootCipherDir.isEmpty()) {
Toast.makeText(this, R.string.enter_volume_path, Toast.LENGTH_SHORT).show()
} else {
val password = edit_password.text.toString().toCharArray()
val passwordConfirm = edit_password_confirm.text.toString().toCharArray()
if (!password.contentEquals(passwordConfirm)) {
Toast.makeText(this, R.string.passwords_mismatch, Toast.LENGTH_SHORT).show()
} else {
object: LoadingTask(this, R.string.loading_msg_create){
override fun doTask(activity: AppCompatActivity) {
val volumePathFile = File(rootCipherDir)
var goodDirectory = false
if (!volumePathFile.isDirectory) {
if (volumePathFile.mkdirs()) {
goodDirectory = true
} else {
stopTaskWithToast(R.string.dir_not_empty)
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.warning)
.setMessage(R.string.create_cant_write_error_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
}
} else {
stopTaskWithToast(R.string.listdir_null_error_msg)
}
}
if (goodDirectory) {
if (GocryptfsVolume.createVolume(rootCipherDir, password, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator)) {
var returnedHash: ByteArray? = null
if (usf_fingerprint && checkbox_save_password.isChecked){
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
}
sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash)
if (sessionID != -1) {
var startExplorerImmediately = true
if (checkbox_remember_path.isChecked) {
val oldSavedVolumesPaths = sharedPrefs.getStringSet(ConstValues.saved_volumes_key, HashSet()) as Set<String>
val editor = sharedPrefs.edit()
val newSavedVolumesPaths = oldSavedVolumesPaths.toMutableList()
if (oldSavedVolumesPaths.contains(rootCipherDir)) {
if (sharedPrefs.getString(rootCipherDir, null) != null){
editor.remove(rootCipherDir)
}
val dirContent = volumePathFile.list()
if (dirContent != null){
if (dirContent.isEmpty()) {
if (volumePathFile.canWrite()){
goodDirectory = true
} else {
newSavedVolumesPaths.add(rootCipherDir)
editor.putStringSet(ConstValues.saved_volumes_key, newSavedVolumesPaths.toSet())
}
editor.apply()
if (checkbox_save_password.isChecked && returnedHash != null){
fingerprintPasswordHashSaver.encryptAndSave(returnedHash, rootCipherDir){ _ ->
stopTask { startExplorer() }
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.warning)
.setMessage(R.string.create_cant_write_error_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
startExplorerImmediately = false
}
}
if (startExplorerImmediately){
stopTask { startExplorer() }
} else {
stopTaskWithToast(R.string.dir_not_empty)
}
} else {
stopTaskWithToast(R.string.open_volume_failed)
stopTaskWithToast(R.string.listdir_null_error_msg)
}
} else {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.error)
.setMessage(R.string.create_volume_failed)
.setPositiveButton(R.string.ok, null)
.show()
}
if (goodDirectory) {
if (GocryptfsVolume.createVolume(rootCipherDir, password, GocryptfsVolume.ScryptDefaultLogN, ConstValues.creator)) {
var returnedHash: ByteArray? = null
if (usf_fingerprint && checkbox_save_password.isChecked){
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
}
sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash)
if (sessionID != -1) {
var startExplorerImmediately = true
if (checkbox_remember_path.isChecked) {
val oldSavedVolumesPaths = sharedPrefs.getStringSet(ConstValues.saved_volumes_key, HashSet()) as Set<String>
val editor = sharedPrefs.edit()
val newSavedVolumesPaths = oldSavedVolumesPaths.toMutableList()
if (oldSavedVolumesPaths.contains(rootCipherDir)) {
if (sharedPrefs.getString(rootCipherDir, null) != null){
editor.remove(rootCipherDir)
}
} else {
newSavedVolumesPaths.add(rootCipherDir)
editor.putStringSet(ConstValues.saved_volumes_key, newSavedVolumesPaths.toSet())
}
editor.apply()
if (checkbox_save_password.isChecked && returnedHash != null){
fingerprintPasswordHashSaver.encryptAndSave(returnedHash, rootCipherDir){ _ ->
stopTask { startExplorer() }
}
startExplorerImmediately = false
}
}
if (startExplorerImmediately){
stopTask { startExplorer() }
}
} else {
stopTaskWithToast(R.string.open_volume_failed)
}
} else {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.error)
.setMessage(R.string.create_volume_failed)
.setPositiveButton(R.string.ok, null)
.show()
}
}
}
}
override fun doFinally(activity: AppCompatActivity) {
Arrays.fill(password, 0.toChar())
Arrays.fill(passwordConfirm, 0.toChar())
}
}
Arrays.fill(password, 0.toChar())
Arrays.fill(passwordConfirm, 0.toChar())
}
}
}

View File

@ -7,8 +7,16 @@ import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.widget.AdapterView.OnItemClickListener
import android.widget.Toast
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.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 sushi.hardcore.droidfs.adapters.SavedVolumesAdapter
import sushi.hardcore.droidfs.explorers.ExplorerActivity
@ -82,52 +90,81 @@ class OpenActivity : BaseActivity() {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
if (requestCode == PICK_DIRECTORY_REQUEST_CODE) {
if (data != null) {
val path = PathUtils.getFullPathFromTreeUri(data.data, this)
edit_volume_path.setText(path)
if (data?.data != null) {
if (PathUtils.isTreeUriOnPrimaryStorage(data.data)){
val path = PathUtils.getFullPathFromTreeUri(data.data, this)
if (path != null){
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?) {
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){
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()
var returnedHash: ByteArray? = null
if (usf_fingerprint && checkbox_save_password.isChecked){
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
}
sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash)
if (sessionID != -1) {
var startExplorerImmediately = true
if (checkbox_remember_path.isChecked) {
savedVolumesAdapter.addVolumePath(rootCipherDir)
if (checkbox_save_password.isChecked && returnedHash != null){
fingerprintPasswordHashSaver.encryptAndSave(returnedHash, rootCipherDir) { _ ->
stopTask { startExplorer() }
}
startExplorerImmediately = false
}
}
if (startExplorerImmediately){
stopTask { startExplorer() }
}
} else {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_volume_failed_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
}
Arrays.fill(password, 0.toChar())
val password = edit_password.text.toString().toCharArray()
var returnedHash: ByteArray? = null
if (usf_fingerprint && checkbox_save_password.isChecked){
returnedHash = ByteArray(GocryptfsVolume.KeyLen)
}
sessionID = GocryptfsVolume.init(rootCipherDir, password, null, returnedHash)
if (sessionID != -1) {
var startExplorerImmediately = true
if (checkbox_remember_path.isChecked) {
savedVolumesAdapter.addVolumePath(rootCipherDir)
if (checkbox_save_password.isChecked && returnedHash != null){
fingerprintPasswordHashSaver.encryptAndSave(returnedHash, rootCipherDir) { _ ->
stopTask { startExplorer() }
}
startExplorerImmediately = false
}
}
if (startExplorerImmediately){
stopTask { startExplorer() }
}
} else {
stopTask {
ColoredAlertDialogBuilder(activity)
.setTitle(R.string.open_volume_failed)
.setMessage(R.string.open_volume_failed_msg)
.setPositiveButton(R.string.ok, null)
.show()
}
}
Arrays.fill(password, 0.toChar())
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,11 @@
<resources>
<string name="app_name">DroidFS</string>
<string name="open_volume">OPEN A VOLUME</string>
<string name="create_volume">CREATE A VOLUME</string>
<string name="change_volume_password">CHANGE VOLUME PASSWORD</string>
<string name="open_volume">Open a volume</string>
<string name="create_volume">Create a volume</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_confirm">Password (confirmation):</string>
<string name="volume_path">Volume Path:</string>
@ -163,4 +166,11 @@
<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="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>