Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

移除「外部儲存空間的權限和存取權」 #1394

Draft
wants to merge 38 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9098b7d
feat: remove external storage related permissions
goofyz Apr 10, 2024
e425880
refactor: change working directory to app specific directory
goofyz Apr 10, 2024
952ff74
refactor: ask for directory permission in onboarding instead of whole…
goofyz Apr 10, 2024
f94e19f
refactor: ask and save directory permission in `ProfileFragment`
goofyz Jun 4, 2024
abc04dc
refactor: display error message if directory is not selected
goofyz Apr 10, 2024
f201854
refactor: start `Rime` only if directory is selected
goofyz Apr 10, 2024
902017b
refactor: remove unused external storage permission request
goofyz Apr 10, 2024
d4d5247
refactor: change working directory to app specific directory
goofyz Apr 10, 2024
ce1fdb6
feat: copy files from working directory to app specific directory
goofyz Apr 10, 2024
34e22c1
feat: display loading dialog during files copying
goofyz Apr 10, 2024
98184f2
feat: export modified file to external user directory when schema cha…
goofyz Apr 19, 2024
dc035ec
feat: copy theme files when `Theme` is selected
goofyz Apr 19, 2024
0dd8c84
fix: do not start Rime if path is blank
goofyz May 28, 2024
6400a11
feat: add `ExternalStorageStateReceiver` to listen for storage to mount
goofyz May 28, 2024
b748a4f
feat: add `FolderExport` to export `/sync` Folder to user directory
goofyz May 30, 2024
107e8a3
refactor: move `FolderSync` to `storage`
goofyz May 30, 2024
80d8759
refactor: move sync function to `ShortcutUtils`
goofyz May 30, 2024
87400b4
fix: use `FolderExport` to export files
goofyz May 30, 2024
f93a9df
feat: add `Rime.isStarting` function
goofyz May 31, 2024
f6066a1
fix: only post notification if rime is in `STARTING` state
goofyz May 31, 2024
c9389e0
feat: return `true` if directory is exported successfully
goofyz May 31, 2024
4482d87
feat: check if validity of URI permissions before copying files
goofyz May 31, 2024
fe22ff2
feat: add `MainUiState`
goofyz May 31, 2024
9970def
feat: check and alert user if URI permission is missing
goofyz May 31, 2024
ad248f4
feat: display failed toast if sync failed
goofyz May 31, 2024
1434303
chore: add debug message when directory is not ready
goofyz May 31, 2024
bca5586
feat: add menu to open internal directory using `DocumentUI`
goofyz Jun 4, 2024
f66ecea
fix: show error if `rime` is stopped instead of pending to show schem…
goofyz Jun 5, 2024
59e8062
fix: if share directory is empty, return user directory
goofyz Jun 5, 2024
c2ff072
fix: create parent directory for `default.custom.yaml`
goofyz Jun 5, 2024
2c85228
feat: add `showDefaultButton`, `defaultButtonLabel` to `FolderPickerP…
goofyz Jun 5, 2024
0952ebd
feat: add "Clear" text and do not allow clearing "User Directory"
goofyz Jun 5, 2024
c0ddaea
feat: save `uri` directly and dismiss dialog after folder is selected
goofyz Jun 5, 2024
485300a
feat: display "EMPTY" as text when folder is empty
goofyz Jun 5, 2024
b1eaa54
feat: small editing the style of `folder_picker_dialog`
goofyz Jun 5, 2024
f98dcc7
fix: make sure at least one item is checked if selected `Theme` is no…
goofyz Apr 19, 2024
b6da8b6
refactor: rename `Uri` variable name
goofyz Jun 6, 2024
87870d2
fix: copy theme files to correct directory
goofyz Jun 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name ="android.permission.WAKE_LOCK" />
Expand All @@ -22,7 +19,6 @@ SPDX-License-Identifier: GPL-3.0-or-later
android:allowBackup="true"
android:icon="@mipmap/ic_app_icon"
android:label="@string/trime_app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_app_icon_round"
android:theme="@style/Theme.TrimeAppTheme">

Expand Down
15 changes: 10 additions & 5 deletions app/src/main/java/com/osfans/trime/core/Rime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import com.osfans.trime.data.AppPrefs
import com.osfans.trime.data.base.DataManager
import com.osfans.trime.data.opencc.OpenCCDictManager
import com.osfans.trime.data.schema.SchemaManager
import com.osfans.trime.util.appContext
import com.osfans.trime.util.isStorageAvailable
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
Expand All @@ -32,15 +30,18 @@ class Rime : RimeApi, RimeLifecycleOwner {
override val isReady: Boolean
get() = lifecycle.currentStateFlow.value == RimeLifecycle.State.READY

override val isStarting: Boolean
get() = lifecycle.currentStateFlow.value == RimeLifecycle.State.STARTING

private val dispatcher =
RimeDispatcher(
object : RimeDispatcher.RimeLooper {
override fun nativeStartup(fullCheck: Boolean) {
DataManager.dirFireChange()
DataManager.sync()

val sharedDataDir = AppPrefs.defaultInstance().profile.sharedDataDir
val userDataDir = AppPrefs.defaultInstance().profile.userDataDir
val sharedDataDir = AppPrefs.defaultInstance().profile.getAppShareDir()
val userDataDir = AppPrefs.defaultInstance().profile.getAppUserDir()
Timber.i("Starting up Rime APIs ...")
startupRime(sharedDataDir, userDataDir, fullCheck)

Expand Down Expand Up @@ -84,9 +85,13 @@ class Rime : RimeApi, RimeLifecycleOwner {
Timber.w("Skip starting rime: not at stopped state!")
return
}
if (appContext.isStorageAvailable()) {
if (AppPrefs.defaultInstance().profile.isUserDataDirChosen() &&
AppPrefs.Profile.isAppPathReady()
) {
lifecycleImpl.emitState(RimeLifecycle.State.STARTING)
dispatcher.start(fullCheck)
} else {
Timber.w("Skip starting rime: directory not yet ready")
}
}

Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/osfans/trime/core/RimeApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ interface RimeApi {

val isReady: Boolean

val isStarting: Boolean

suspend fun isEmpty(): Boolean

suspend fun availableSchemata(): Array<SchemaListItem>
Expand Down
35 changes: 22 additions & 13 deletions app/src/main/java/com/osfans/trime/daemon/RimeDaemon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import com.osfans.trime.core.RimeApi
import com.osfans.trime.core.RimeLifecycle
import com.osfans.trime.core.lifecycleScope
import com.osfans.trime.core.whenReady
import com.osfans.trime.daemon.RimeDaemon.createSession
import com.osfans.trime.daemon.RimeDaemon.destroySession
import com.osfans.trime.util.appContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -122,26 +124,33 @@ object RimeDaemon {
private const val CHANNEL_ID = "rime-daemon"
private var restartId = 0

private fun postNotification(id: Int) {
NotificationCompat.Builder(appContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_baseline_sync_24)
.setContentTitle(appContext.getString(R.string.rime_daemon))
.setContentText(appContext.getString(R.string.restarting_rime))
.setOngoing(true)
.setProgress(100, 0, true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build().let { notificationManager.notify(id, it) }
}

/**
* Restart Rime instance to deploy while keep the session
*/
fun restartRime(fullCheck: Boolean = false) =
lock.withLock {
val id = restartId++
NotificationCompat.Builder(appContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_baseline_sync_24)
.setContentTitle(appContext.getString(R.string.rime_daemon))
.setContentText(appContext.getString(R.string.restarting_rime))
.setOngoing(true)
.setProgress(100, 0, true)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.build().let { notificationManager.notify(id, it) }
realRime.finalize()
realRime.startup(fullCheck)
TrimeApplication.getInstance().coroutineScope.launch {
// cancel notification on ready
realRime.lifecycle.whenReady {
notificationManager.cancel(id)

if (realRime.isStarting) {
val id = restartId++
postNotification(id)
TrimeApplication.getInstance().coroutineScope.launch {
// cancel notification on ready
realRime.lifecycle.whenReady {
notificationManager.cancel(id)
}
}
}
}
Expand Down
29 changes: 24 additions & 5 deletions app/src/main/java/com/osfans/trime/data/AppPrefs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ package com.osfans.trime.data
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import com.blankj.utilcode.util.PathUtils
import com.osfans.trime.R
import com.osfans.trime.data.base.DataManager
import com.osfans.trime.ime.enums.FullscreenMode
import com.osfans.trime.ime.enums.InlinePreeditMode
import com.osfans.trime.ime.keyboard.KeyboardPrefs
Expand Down Expand Up @@ -347,13 +347,30 @@ class AppPrefs(
const val TIMING_SYNC_TRIGGER_TIME = "profile_timing_sync_trigger_time"
const val LAST_SYNC_STATUS = "profile_last_sync_status"
const val LAST_BACKGROUND_SYNC = "profile_last_background_sync"
const val URI_PREFIX = "content://com.android.externalstorage.documents/"

fun getAppPath(): String {
return PathUtils.getExternalAppFilesPath()
}

fun isAppPathReady() = getAppPath().isNotBlank()
}

fun getAppUserDir() = getAppPath() + "/user"

fun getAppShareDir(): String {
return if (sharedDataDirUri.isNotEmpty() && sharedDataDirUri != userDataDirUri) {
getAppPath() + "/share"
} else {
getAppUserDir()
}
}

var sharedDataDir: String
get() = prefs.getPref(SHARED_DATA_DIR, DataManager.defaultDataDirectory.path)
var sharedDataDirUri: String
get() = prefs.getPref(SHARED_DATA_DIR, "")
set(v) = prefs.setPref(SHARED_DATA_DIR, v)
var userDataDir: String
get() = prefs.getPref(USER_DATA_DIR, DataManager.defaultDataDirectory.path)
var userDataDirUri: String
get() = prefs.getPref(USER_DATA_DIR, "")
set(v) = prefs.setPref(USER_DATA_DIR, v)
var syncBackgroundEnabled: Boolean
get() = prefs.getPref(SYNC_BACKGROUND_ENABLED, false)
Expand All @@ -370,6 +387,8 @@ class AppPrefs(
var lastBackgroundSync: String
get() = prefs.getPref(LAST_BACKGROUND_SYNC, "")
set(v) = prefs.setPref(LAST_BACKGROUND_SYNC, v)

fun isUserDataDirChosen() = userDataDirUri.isNotBlank() && userDataDirUri.startsWith(URI_PREFIX)
}

class Clipboard(private val prefs: AppPrefs) {
Expand Down
9 changes: 3 additions & 6 deletions app/src/main/java/com/osfans/trime/data/base/DataManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package com.osfans.trime.data.base

import android.content.res.AssetManager
import android.os.Build
import android.os.Environment
import com.osfans.trime.data.AppPrefs
import com.osfans.trime.util.FileUtils
import com.osfans.trime.util.ResourceUtils
Expand Down Expand Up @@ -50,8 +49,6 @@ object DataManager {

private val prefs get() = AppPrefs.defaultInstance()

val defaultDataDirectory = File(Environment.getExternalStorageDirectory(), "rime")

private val onDataDirChangeListeners = WeakHashSet<OnDataDirChangeListener>()

fun interface OnDataDirChangeListener {
Expand All @@ -72,11 +69,11 @@ object DataManager {

@JvmStatic
val sharedDataDir
get() = File(prefs.profile.sharedDataDir)
get() = File(prefs.profile.getAppShareDir())

@JvmStatic
val userDataDir
get() = File(prefs.profile.userDataDir)
get() = File(prefs.profile.getAppUserDir())

/**
* Return the absolute path of the compiled config file
Expand Down Expand Up @@ -126,7 +123,7 @@ object DataManager {
File(sharedDataDir, DEFAULT_CUSTOM_FILE_NAME).let {
if (!it.exists()) {
Timber.d("Creating empty default.custom.yaml")
it.bufferedWriter().use { w -> w.write("") }
it.also { it.parentFile?.mkdirs() }.bufferedWriter().use { w -> w.write("") }
}
}

Expand Down
119 changes: 119 additions & 0 deletions app/src/main/java/com/osfans/trime/data/storage/FolderExport.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.osfans.trime.data.storage

import android.content.Context
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import com.osfans.trime.data.AppPrefs
import com.osfans.trime.util.appContext
import timber.log.Timber
import java.io.File

class FolderExport(private val context: Context, private val docUriStr: String) {
suspend fun exportDir(dirPath: File): Boolean {
return runCatching {
DocumentFile.fromTreeUri(appContext, docUriStr.toUri())?.let { dir ->
val dirDoc =
dir.findFile(dirPath.name)?.takeIf { it.isDirectory && it.canWrite() }
?: dir.createDirectory(dirPath.name)

dirDoc?.let {
recursiveExport(dirPath, dirDoc)
} ?: run {
Timber.e("Error, cannot create export folder, %s", dirPath.name)
}
}
true
}.onFailure {
Timber.e(it, "Error in export")
}.getOrDefault(false)
}

suspend fun exportModifiedFiles(fileNames: Array<File>) {
runCatching {
DocumentFile.fromTreeUri(appContext, docUriStr.toUri())?.let {
fileNames.forEach { fileName ->
export(fileName, it)
}
}
}.onFailure {
Timber.e(it, "Error in export")
}
}

private suspend fun recursiveExport(
sourcePath: File,
targetDir: DocumentFile,
) {
sourcePath.listFiles()?.forEach { file ->
if (file.isFile) {
export(file, targetDir)
} else if (file.isDirectory) {
targetDir.runCatching {
val docFile =
this.findFile(file.name)?.takeIf { it.isDirectory && it.canWrite() }
?: this.createDirectory(file.name)

docFile?.let {
recursiveExport(file, it)
}
}.onFailure {
Timber.e(it)
}
}
}
}

private suspend fun export(
fileName: File,
targetDir: DocumentFile,
) {
runCatching {
val docFile =
targetDir.findFile(fileName.name)?.takeIf { it.isFile && it.canWrite() }
?: targetDir.createFile("*/*", fileName.name)
docFile?.let { doc ->
copyToUri(fileName, doc)
} ?: run {
Timber.e("Cannot export file: %s", fileName.name)
}
}.onFailure {
Timber.e(it, "Uri Error")
}
}

private fun copyToUri(
sourceFile: File,
targetDoc: DocumentFile,
) {
val oss = context.contentResolver.openOutputStream(targetDoc.uri, "wt")
oss?.use {
sourceFile.inputStream().apply {
copyTo(oss)

close()
}
}
}

companion object {
suspend fun exportModifiedFiles() {
val profile = AppPrefs.defaultInstance().profile
val userDirUri = profile.userDataDirUri

val f1 = File(profile.getAppUserDir(), "default.custom.yaml")
val f2 = File(profile.getAppUserDir(), "user.yaml")

FolderExport(appContext, userDirUri).exportModifiedFiles(arrayOf(f1, f2))
}

suspend fun exportSyncDir(): Boolean {
val profile = AppPrefs.defaultInstance().profile
val userDirUri = profile.userDataDirUri

val dir = "sync"
val dirFile = File(profile.getAppUserDir(), dir)

return FolderExport(appContext, userDirUri).exportDir(dirFile)
}
}
}
Loading
Loading