= Build.VERSION_CODES.R) {
+ if (!Environment.isExternalStorageManager()) {
+ try {
+ val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
+ intent.data = Uri.parse("package:${activity.packageName}")
+ activity.startActivityForResult(intent, REQUEST_MANAGE_STORAGE)
+ ret.put("manageStorage", "denied")
+ invoke.resolve(ret)
+ } catch (e: Exception) {
+ val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
+ activity.startActivity(intent)
+ ret.put("manageStorage", "denied")
+ invoke.resolve(ret)
+ }
+ } else {
+ ret.put("manageStorage", "granted")
+ invoke.resolve(ret)
+ }
+ } else {
+ val readPermission = ContextCompat.checkSelfPermission(
+ activity,
+ Manifest.permission.READ_EXTERNAL_STORAGE
+ )
+ val writePermission = ContextCompat.checkSelfPermission(
+ activity,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ )
+ if (readPermission == PackageManager.PERMISSION_GRANTED &&
+ writePermission == PackageManager.PERMISSION_GRANTED) {
+ ret.put("manageStorage", "granted")
+ invoke.resolve(ret)
+ } else {
+ ActivityCompat.requestPermissions(
+ activity,
+ arrayOf(
+ Manifest.permission.READ_EXTERNAL_STORAGE,
+ Manifest.permission.WRITE_EXTERNAL_STORAGE
+ ),
+ REQUEST_MANAGE_STORAGE
+ )
+ ret.put("manageStorage", "prompt")
+ invoke.resolve(ret)
+ }
+ }
+ }
}
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs
index 3a42ecbe..30b21d1e 100644
--- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/build.rs
@@ -15,6 +15,9 @@ const COMMANDS: &[&str] = &[
"iap_restore_purchases",
"get_system_color_scheme",
"get_safe_area_insets",
+ "request_manage_storage_permission",
+ "checkPermissions",
+ "requestPermissions",
];
fn main() {
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/checkPermissions.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/checkPermissions.toml
new file mode 100644
index 00000000..dd209449
--- /dev/null
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/checkPermissions.toml
@@ -0,0 +1,13 @@
+# Automatically generated - DO NOT EDIT!
+
+"$schema" = "../../schemas/schema.json"
+
+[[permission]]
+identifier = "allow-checkPermissions"
+description = "Enables the checkPermissions command without any pre-configured scope."
+commands.allow = ["checkPermissions"]
+
+[[permission]]
+identifier = "deny-checkPermissions"
+description = "Denies the checkPermissions command without any pre-configured scope."
+commands.deny = ["checkPermissions"]
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/requestPermissions.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/requestPermissions.toml
new file mode 100644
index 00000000..b3238d63
--- /dev/null
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/requestPermissions.toml
@@ -0,0 +1,13 @@
+# Automatically generated - DO NOT EDIT!
+
+"$schema" = "../../schemas/schema.json"
+
+[[permission]]
+identifier = "allow-requestPermissions"
+description = "Enables the requestPermissions command without any pre-configured scope."
+commands.allow = ["requestPermissions"]
+
+[[permission]]
+identifier = "deny-requestPermissions"
+description = "Denies the requestPermissions command without any pre-configured scope."
+commands.deny = ["requestPermissions"]
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/request_manage_storage_permission.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/request_manage_storage_permission.toml
new file mode 100644
index 00000000..634a2199
--- /dev/null
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/commands/request_manage_storage_permission.toml
@@ -0,0 +1,13 @@
+# Automatically generated - DO NOT EDIT!
+
+"$schema" = "../../schemas/schema.json"
+
+[[permission]]
+identifier = "allow-request-manage-storage-permission"
+description = "Enables the request_manage_storage_permission command without any pre-configured scope."
+commands.allow = ["request_manage_storage_permission"]
+
+[[permission]]
+identifier = "deny-request-manage-storage-permission"
+description = "Denies the request_manage_storage_permission command without any pre-configured scope."
+commands.deny = ["request_manage_storage_permission"]
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md
index fb3332b9..9dd17e8f 100644
--- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/autogenerated/reference.md
@@ -20,6 +20,9 @@ Default permissions for the plugin
- `allow-iap-restore-purchases`
- `allow-get-system-color-scheme`
- `allow-get-safe-area-insets`
+- `allow-request-manage-storage-permission`
+- `allow-checkPermissions`
+- `allow-requestPermissions`
## Permission Table
@@ -85,6 +88,32 @@ Denies the auth_with_safari command without any pre-configured scope.
|
+`native-bridge:allow-checkPermissions`
+
+ |
+
+
+Enables the checkPermissions command without any pre-configured scope.
+
+ |
+
+
+
+|
+
+`native-bridge:deny-checkPermissions`
+
+ |
+
+
+Denies the checkPermissions command without any pre-configured scope.
+
+ |
+
+
+
+|
+
`native-bridge:allow-copy-uri-to-path`
|
@@ -397,6 +426,58 @@ Denies the lock_screen_orientation command without any pre-configured scope.
|
+`native-bridge:allow-requestPermissions`
+
+ |
+
+
+Enables the requestPermissions command without any pre-configured scope.
+
+ |
+
+
+
+|
+
+`native-bridge:deny-requestPermissions`
+
+ |
+
+
+Denies the requestPermissions command without any pre-configured scope.
+
+ |
+
+
+
+|
+
+`native-bridge:allow-request-manage-storage-permission`
+
+ |
+
+
+Enables the request_manage_storage_permission command without any pre-configured scope.
+
+ |
+
+
+
+|
+
+`native-bridge:deny-request-manage-storage-permission`
+
+ |
+
+
+Denies the request_manage_storage_permission command without any pre-configured scope.
+
+ |
+
+
+
+|
+
`native-bridge:allow-set-system-ui-visibility`
|
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml
index a88c994a..0e3ff07a 100644
--- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/default.toml
@@ -17,4 +17,7 @@ permissions = [
"allow-iap-restore-purchases",
"allow-get-system-color-scheme",
"allow-get-safe-area-insets",
+ "allow-request-manage-storage-permission",
+ "allow-checkPermissions",
+ "allow-requestPermissions",
]
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json
index 95dbe93c..d5588e4e 100644
--- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/permissions/schemas/schema.json
@@ -318,6 +318,18 @@
"const": "deny-auth-with-safari",
"markdownDescription": "Denies the auth_with_safari command without any pre-configured scope."
},
+ {
+ "description": "Enables the checkPermissions command without any pre-configured scope.",
+ "type": "string",
+ "const": "allow-checkPermissions",
+ "markdownDescription": "Enables the checkPermissions command without any pre-configured scope."
+ },
+ {
+ "description": "Denies the checkPermissions command without any pre-configured scope.",
+ "type": "string",
+ "const": "deny-checkPermissions",
+ "markdownDescription": "Denies the checkPermissions command without any pre-configured scope."
+ },
{
"description": "Enables the copy_uri_to_path command without any pre-configured scope.",
"type": "string",
@@ -462,6 +474,30 @@
"const": "deny-lock-screen-orientation",
"markdownDescription": "Denies the lock_screen_orientation command without any pre-configured scope."
},
+ {
+ "description": "Enables the requestPermissions command without any pre-configured scope.",
+ "type": "string",
+ "const": "allow-requestPermissions",
+ "markdownDescription": "Enables the requestPermissions command without any pre-configured scope."
+ },
+ {
+ "description": "Denies the requestPermissions command without any pre-configured scope.",
+ "type": "string",
+ "const": "deny-requestPermissions",
+ "markdownDescription": "Denies the requestPermissions command without any pre-configured scope."
+ },
+ {
+ "description": "Enables the request_manage_storage_permission command without any pre-configured scope.",
+ "type": "string",
+ "const": "allow-request-manage-storage-permission",
+ "markdownDescription": "Enables the request_manage_storage_permission command without any pre-configured scope."
+ },
+ {
+ "description": "Denies the request_manage_storage_permission command without any pre-configured scope.",
+ "type": "string",
+ "const": "deny-request-manage-storage-permission",
+ "markdownDescription": "Denies the request_manage_storage_permission command without any pre-configured scope."
+ },
{
"description": "Enables the set_system_ui_visibility command without any pre-configured scope.",
"type": "string",
@@ -487,10 +523,10 @@
"markdownDescription": "Denies the use_background_audio command without any pre-configured scope."
},
{
- "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`",
+ "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
"type": "string",
"const": "default",
- "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`"
+ "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`\n- `allow-install-package`\n- `allow-set-system-ui-visibility`\n- `allow-get-status-bar-height`\n- `allow-get-sys-fonts-list`\n- `allow-intercept-keys`\n- `allow-lock-screen-orientation`\n- `allow-iap-initialize`\n- `allow-iap-fetch-products`\n- `allow-iap-purchase-product`\n- `allow-iap-restore-purchases`\n- `allow-get-system-color-scheme`\n- `allow-get-safe-area-insets`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
}
]
}
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs
index 8cda521f..021c2130 100644
--- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/commands.rs
@@ -126,3 +126,10 @@ pub(crate) async fn get_safe_area_insets(
) -> Result {
app.native_bridge().get_safe_area_insets()
}
+
+#[command]
+pub(crate) async fn request_manage_storage_permission(
+ app: AppHandle,
+) -> Result {
+ app.native_bridge().request_manage_storage_permission()
+}
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs
index 77f5cc00..f20f0c87 100644
--- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/desktop.rs
@@ -106,4 +106,10 @@ impl NativeBridge {
pub fn get_safe_area_insets(&self) -> crate::Result {
Err(crate::Error::UnsupportedPlatformError)
}
+
+ pub fn request_manage_storage_permission(
+ &self,
+ ) -> crate::Result {
+ Err(crate::Error::UnsupportedPlatformError)
+ }
}
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs
index 35cc282f..e8cf47ef 100644
--- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/lib.rs
@@ -53,6 +53,7 @@ pub fn init() -> TauriPlugin {
commands::iap_restore_purchases,
commands::get_system_color_scheme,
commands::get_safe_area_insets,
+ commands::request_manage_storage_permission,
])
.setup(|app, api| {
#[cfg(mobile)]
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs
index ecb99986..3f50da39 100644
--- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/mobile.rs
@@ -169,3 +169,13 @@ impl NativeBridge {
.map_err(Into::into)
}
}
+
+impl NativeBridge {
+ pub fn request_manage_storage_permission(
+ &self,
+ ) -> crate::Result {
+ self.0
+ .run_mobile_plugin("request_manage_storage_permission", ())
+ .map_err(Into::into)
+ }
+}
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs
index 8254f0bf..d8b446b7 100644
--- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-bridge/src/models.rs
@@ -167,3 +167,9 @@ pub struct GetSafeAreaInsetsResponse {
pub left: f64,
pub right: f64,
}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct RequestManageStoragePermissionResponse {
+ pub manage_storage: String, // "granted", "denied", or "prompt"
+}
diff --git a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/src/main/java/NativeTTSPlugin.kt b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/src/main/java/NativeTTSPlugin.kt
index ac65d1e5..71c88662 100644
--- a/apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/src/main/java/NativeTTSPlugin.kt
+++ b/apps/readest-app/src-tauri/plugins/tauri-plugin-native-tts/android/src/main/java/NativeTTSPlugin.kt
@@ -150,10 +150,10 @@ class MediaForegroundService : Service() {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("TTS Media Session")
.setContentText("Text-to-speech playback active")
- .setSmallIcon(android.R.drawable.ic_media_play) // Replace with your icon
+ .setSmallIcon(android.R.drawable.ic_media_play)
.setOngoing(true)
.setSilent(true)
- .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setPriority(NotificationCompat.PRIORITY_MIN)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.build()
diff --git a/apps/readest-app/src/app/library/components/MigrateDataWindow.tsx b/apps/readest-app/src/app/library/components/MigrateDataWindow.tsx
index 9453fa90..1eaa741e 100644
--- a/apps/readest-app/src/app/library/components/MigrateDataWindow.tsx
+++ b/apps/readest-app/src/app/library/components/MigrateDataWindow.tsx
@@ -6,7 +6,8 @@ import {
RiErrorWarningFill,
RiLoader2Line,
} from 'react-icons/ri';
-import { join } from '@tauri-apps/api/path';
+import { documentDir, join } from '@tauri-apps/api/path';
+import { invoke, PermissionState } from '@tauri-apps/api/core';
import { relaunch } from '@tauri-apps/plugin-process';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
@@ -40,6 +41,10 @@ interface MigrationProgress {
currentFile?: string;
}
+interface Permissions {
+ manageStorage: PermissionState;
+}
+
export const MigrateDataWindow = () => {
const _ = useTranslation();
const { appService, envConfig } = useEnv();
@@ -56,12 +61,14 @@ export const MigrateDataWindow = () => {
const [filesToMigrate, setFilesToMigrate] = useState([]);
const [currentDirFileCount, setCurrentDirFileCount] = useState('');
const [currentDirFileSize, setCurrentDirFileSize] = useState(0);
+ const [androidNewDirs, setAndroidNewDirs] = useState<{ path: string; label: string }[]>([]);
useEffect(() => {
const handleCustomEvent = (event: CustomEvent) => {
setIsOpen(event.detail.visible);
if (event.detail.visible) {
loadCurrentDataDir();
+ loadAndroidDirs();
}
};
@@ -93,6 +100,27 @@ export const MigrateDataWindow = () => {
}
};
+ const loadAndroidDirs = async () => {
+ const sdcardDirs = [
+ { path: '/storage/emulated/0', label: '/sdcard' },
+ { path: '/storage/emulated/0/Books', label: '/sdcard/Books' },
+ { path: '/storage/emulated/0/Documents', label: '/sdcard/Documents' },
+ { path: '/storage/emulated/0/Download', label: '/sdcard/Download' },
+ ];
+ try {
+ if (appService?.isAndroidApp) {
+ const localDocumentDir = await documentDir();
+ setAndroidNewDirs([
+ // For Google Play version we won't request permission to access root of /sdcard
+ ...(appService?.distChannel === 'playstore' ? [] : sdcardDirs),
+ { path: localDocumentDir, label: '/sdcard/APPDATA/Documents' },
+ ]);
+ }
+ } catch (error) {
+ console.error('Error loading app local data directory:', error);
+ }
+ };
+
const handleSelectNewDir = async () => {
setMigrationStatus('selecting');
setErrorMessage('');
@@ -117,6 +145,16 @@ export const MigrateDataWindow = () => {
const handleSelectedNewDir = async (dir: string) => {
setErrorMessage('');
+ if (!dir.includes('Android/data')) {
+ let permission = await invoke('plugin:native-bridge|checkPermissions');
+ if (permission.manageStorage !== 'granted') {
+ permission = await invoke(
+ 'plugin:native-bridge|request_manage_storage_permission',
+ );
+ }
+ if (permission.manageStorage !== 'granted') return;
+ }
+
try {
const newDataDir = await join(dir, DATA_SUBDIR);
await appService?.createDir(newDataDir, 'None', true);
@@ -221,11 +259,6 @@ export const MigrateDataWindow = () => {
const fileRevealLabel =
FILE_REVEAL_LABELS[osPlatform as FILE_REVEAL_PLATFORMS] || FILE_REVEAL_LABELS.default;
- const androidNewDirs = [
- { path: '/storage/emulated/0/Documents' },
- { path: '/storage/emulated/0/Download' },
- ];
-
return (