Skip to content

Commit

Permalink
Filesync compatibility with Android <= 9 (fixes johannesjo#36)
Browse files Browse the repository at this point in the history
Signed-off-by: Stephen L. <lrq3000@gmail.com>
  • Loading branch information
lrq3000 committed Feb 4, 2023
1 parent 7510140 commit 8f2cc9d
Showing 1 changed file with 83 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,6 @@ abstract class CommonJavaScriptInterface(
// Mandatory for Activity, but not for Fragment & ComponentActivity
Log.d("SuperProductivity", "onActivityResult")
activity.storageHelper.storage.onActivityResult(requestCode, resultCode, data)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// Once permissions are granted, callback web application to continue execution
callJavaScriptFunction(
FN_PREFIX + "grantFilePermissionCallBack('" + requestCode + "')"
)
}
}

@Suppress("unused")
Expand Down Expand Up @@ -331,22 +325,25 @@ abstract class CommonJavaScriptInterface(
@JavascriptInterface
fun getFileRev(filePath: String): String {
Log.d("SuperProductivity", "getFileRev")
// Get folder path
val sp = activity.getPreferences(Context.MODE_PRIVATE)
val folderPath = sp.getString("filesyncFolder", "") ?: ""
// Build fullFilePath from folder path and filepath
val fullFilePath = "$folderPath/$filePath"
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Scoped storage permission management for Android 10+
// Get folder path
val sp = activity.getPreferences(Context.MODE_PRIVATE)
val folderPath = sp.getString("filesyncFolder", "") ?: ""
// Build fullFilePath from folder path and filepath
val fullFilePath = "$folderPath/$filePath"
// Load file
val file = DocumentFileCompat.fromFullPath(activity, fullFilePath, requiresWriteAccess=false)
// Get last modified date
val lastModif = file?.lastModified().toString()
Log.d("SuperProductivity", "getFileRev lastModified: $lastModif")
lastModif
} else {
val file = File(filePath)
file.lastModified().toString()
val file = File(fullFilePath)
// Get last modified date
val lastModif = file.lastModified().toString()
Log.d("SuperProductivity", "getFileRev lastModified: $lastModif")
lastModif
}
}

Expand All @@ -355,22 +352,30 @@ abstract class CommonJavaScriptInterface(
fun readFile(filePath: String): String {
// Read a file, most likely the filesync database
Log.d("SuperProductivity", "readFile")


// Get folder path
val sp = activity.getPreferences(Context.MODE_PRIVATE)
val folderPath = sp.getString("filesyncFolder", "") ?: ""
// Build fullFilePath from folder path and filepath
val fullFilePath = "$folderPath/$filePath"
Log.d(
"SuperProductivity",
"readFile: trying to read from fullFilePath: " + fullFilePath
)
// Open file in read only mode and an InputStream
// Make a reader pointing to the input file
val reader =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Scoped storage permission management for Android 10+
// Get folder path
val sp = activity.getPreferences(Context.MODE_PRIVATE)
val folderPath = sp.getString("filesyncFolder", "") ?: ""
// Build fullFilePath from folder path and filepath
val fullFilePath = "$folderPath/$filePath"
Log.d("SuperProductivity", "readFile: trying to read from fullFilePath: " + fullFilePath)
// Open file in read only mode and an InputStream
val file = DocumentFileCompat.fromFullPath(activity, fullFilePath, requiresWriteAccess=false)
val file = DocumentFileCompat.fromFullPath(
activity,
fullFilePath,
requiresWriteAccess = false
)
file?.openInputStream(activity)?.reader()
} else {
BufferedReader(FileReader(filePath))
// Older versions of Android <= 9 don't need scoped storage management
BufferedReader(FileReader(fullFilePath))
}

// Use a StringBuilder to rebuild the input file's content but replace the line returns with current OS's line returns
Expand All @@ -397,34 +402,29 @@ abstract class CommonJavaScriptInterface(
@Suppress("unused")
@JavascriptInterface
fun writeFile(filePath: String, data: String) {
val writer: Writer =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Scoped storage permission management for Android 10+
Log.d("SuperProductivity", "writeFile: trying to save to filePath: " + filePath)
// Get folder path
val sp = activity.getPreferences(Context.MODE_PRIVATE)
val folderPath = sp.getString("filesyncFolder", "") ?: ""
// Build fullFilePath from folder path and filepath
val fullFilePath = "$folderPath/$filePath"
Log.d("SuperProductivity", "writeFile: trying to save to fullFilePath: " + fullFilePath)
// Open file with write access, using SimpleStorage helper wrapper DocumentFileCompat
var file = DocumentFileCompat.fromFullPath(activity, fullFilePath, requiresWriteAccess=true, considerRawFile=true)
if ((file == null) || (!file.exists())) { // if file does not exist, we create it
Log.d("SuperProductivity", "writeFile: file does not exist, try to create it")
val folder = DocumentFileCompat.fromFullPath(activity, folderPath, requiresWriteAccess=true)
Log.d("SuperProductivity", "writeFile: do we have access to parentFolder? " + folder.toString())
file = folder!!.makeFile(activity, filePath, mode=CreateMode.REPLACE) // do NOT specify a mimeType, otherwise Android will force a file extension
}
Log.d("SuperProductivity", "writeFile: erase file content by recreating it")
file = file?.recreateFile(activity) // erase content first by recreating file. For some reason, DocumentFileCompat.fromFullPath(requiresWriteAccess=true) and openOutputStream(append=false) only open the file in append mode, so we need to recreate the file to truncate its content first
// Open an OutputStream to the file without append mode (so we write from the start of the file)
Log.d("SuperProductivity", "writeFile: try to openOutputStream")
file?.openOutputStream(activity, append=false)!!.writer()
} else {
BufferedWriter(FileWriter(filePath))
Log.d("SuperProductivity", "writeFile: trying to save to filePath: " + filePath)
// Get folder path
val sp = activity.getPreferences(Context.MODE_PRIVATE)
val folderPath = sp.getString("filesyncFolder", "") ?: ""
// Build fullFilePath from folder path and filepath
val fullFilePath = "$folderPath/$filePath"
Log.d("SuperProductivity", "writeFile: trying to save to fullFilePath: " + fullFilePath)
// Scoped storage permission management for Android 10+, but also works for Android < 10
// Open file with write access, using SimpleStorage helper wrapper DocumentFileCompat
var file = DocumentFileCompat.fromFullPath(activity, fullFilePath, requiresWriteAccess=true, considerRawFile=true)
if ((file == null) || (!file.exists())) { // if file does not exist, we create it
Log.d("SuperProductivity", "writeFile: file does not exist, try to create it")
val folder = DocumentFileCompat.fromFullPath(activity, folderPath, requiresWriteAccess=true)
Log.d("SuperProductivity", "writeFile: do we have access to parentFolder? " + folder.toString())
file = folder!!.makeFile(activity, filePath, mode=CreateMode.REPLACE) // do NOT specify a mimeType, otherwise Android will force a file extension
}
Log.d("SuperProductivity", "writeFile: erase file content by recreating it")
file = file?.recreateFile(activity) // erase content first by recreating file. For some reason, DocumentFileCompat.fromFullPath(requiresWriteAccess=true) and openOutputStream(append=false) only open the file in append mode, so we need to recreate the file to truncate its content first
// Open a writer to an OutputStream to the file without append mode (so we write from the start of the file)
Log.d("SuperProductivity", "writeFile: try to openOutputStream")
val writer: Writer =file?.openOutputStream(activity, append=false)!!.writer()
try {
Log.d("SuperProductivity", "writeFile: try to write data into file: " + data)
Log.d("SuperProductivity", "writeFile: try to write data into file: $data")
writer.write(data)
Log.d("SuperProductivity", "writeFile: write apparently successful!")
} catch (e: Exception) {
Expand Down Expand Up @@ -493,46 +493,47 @@ abstract class CommonJavaScriptInterface(
@Suppress("unused")
@JavascriptInterface
fun grantFilePermission(requestId: String) {
// For Android >= 10, use scoped storage via SimpleStorage library to get the permission to write files in a folder
// Note that SimpleStorage takes care of all the gritty technical details, including whether the user must pick a root path BEFORE selecting the folder they want to store in, everything is explained to the user
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Log.d("SuperProductivity", "Before SimpleStorageHelper callback func def")
// Register a callback with SimpleStorage when a folder is picked
activity.storageHelper.onFolderSelected =
{ requestCode, root -> // could also use simpleStorageHelper.onStorageAccessGranted()
Log.d("SuperProductivity", "Success Folder Pick! Now saving...")
// Get absolute path to folder
val fpath = root.getAbsolutePath(activity)
// Open preferences to save folder to path
val sp = activity.getPreferences(Context.MODE_PRIVATE)
sp.edit().putString("filesyncFolder", fpath).apply()
// Once permissions are granted, callback web application to continue execution
callJavaScriptFunction(
FN_PREFIX + "grantFilePermissionCallBack('" + requestId + "')"
)
}
// Open folder picker via SimpleStorage, this will request the necessary scoped storage permission
// Note that even though we get permissions, we need to only write DocumentFile files, not MediaStore files, because the latter are not meant to be reopened in the future so we can lose permission at anytime once they are written once, see: https://github.com/anggrayudi/SimpleStorage/issues/103
Log.d("SuperProductivity", "Get Storage Access permission")
activity.storageHelper.openFolderPicker(
// We could also use simpleStorageHelper.requestStorageAccess()
initialPath = FileFullPath(
activity,
StorageId.PRIMARY,
"SupProd"
), // SimpleStorage.externalStoragePath if we want to default to sdcard
// to force pick a specific folder and none others, use these arguments for simpleStorageHelper.requestStorageAccess():
//expectedStorageType = StorageType.EXTERNAL,
//expectedBasePath = "SupProd"
)
} else {
// For Android < 10, ask for permission to access the whole storage
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
ActivityCompat.requestPermissions(
activity, arrayOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
), 1
)
}
// For Android >= 10, use scoped storage via SimpleStorage library to get the permission to write files in a folder
// For Android < 10, SimpleStorage serves as a simple folder path picker, so that we still save where the user want look for a database file
// Note that SimpleStorage takes care of all the gritty technical details, including whether the user must pick a root path BEFORE selecting the folder they want to store in, everything is explained to the user
Log.d("SuperProductivity", "Before SimpleStorageHelper callback func def")
// Register a callback with SimpleStorage when a folder is picked
activity.storageHelper.onFolderSelected =
{ requestCode, root -> // could also use simpleStorageHelper.onStorageAccessGranted()
Log.d("SuperProductivity", "Success Folder Pick! Now saving...")
// Get absolute path to folder
val fpath = root.getAbsolutePath(activity)
// Open preferences to save folder to path
val sp = activity.getPreferences(Context.MODE_PRIVATE)
sp.edit().putString("filesyncFolder", fpath).apply()
// Once permissions are granted, callback web application to continue execution
callJavaScriptFunction(
FN_PREFIX + "grantFilePermissionCallBack('" + requestId + "')"
)
}
// Open folder picker via SimpleStorage, this will request the necessary scoped storage permission
// Note that even though we get permissions, we need to only write DocumentFile files, not MediaStore files, because the latter are not meant to be reopened in the future so we can lose permission at anytime once they are written once, see: https://github.com/anggrayudi/SimpleStorage/issues/103
Log.d("SuperProductivity", "Get Storage Access permission")
activity.storageHelper.openFolderPicker(
// We could also use simpleStorageHelper.requestStorageAccess()
initialPath = FileFullPath(
activity,
StorageId.PRIMARY,
"SupProd"
), // SimpleStorage.externalStoragePath if we want to default to sdcard
// to force pick a specific folder and none others, use these arguments for simpleStorageHelper.requestStorageAccess():
//expectedStorageType = StorageType.EXTERNAL,
//expectedBasePath = "SupProd"
)
}

protected fun callJavaScriptFunction(script: String) {
Expand Down

0 comments on commit 8f2cc9d

Please sign in to comment.