feat(android): support custom data location on external sdcard (#2292)

This commit is contained in:
Huang Xin
2025-10-22 16:14:41 +08:00
committed by GitHub
parent 34a5e58872
commit 50d7f9a9dd
13 changed files with 131 additions and 9 deletions
@@ -588,6 +588,23 @@ class NativeBridgePlugin(private val activity: Activity): Plugin(activity) {
}
}
@Command
fun get_external_sdcard_path(invoke: Invoke) {
val result = JSObject()
val externalDirs = activity.getExternalFilesDirs(null)
for (file in externalDirs) {
if (file != null && Environment.isExternalStorageRemovable(file)) {
val pathParts = file.absolutePath.split("/Android/")
if (pathParts.isNotEmpty()) {
result.put("path", pathParts[0])
invoke.resolve(result)
}
}
}
result.put("path", null)
invoke.resolve(result)
}
@Command
fun request_manage_storage_permission(invoke: Invoke) {
val ret = JSObject()
@@ -17,6 +17,7 @@ const COMMANDS: &[&str] = &[
"get_safe_area_insets",
"get_screen_brightness",
"set_screen_brightness",
"get_external_sdcard_path",
"request_manage_storage_permission",
"checkPermissions",
"requestPermissions",
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-get-external-sdcard-path"
description = "Enables the get_external_sdcard_path command without any pre-configured scope."
commands.allow = ["get_external_sdcard_path"]
[[permission]]
identifier = "deny-get-external-sdcard-path"
description = "Denies the get_external_sdcard_path command without any pre-configured scope."
commands.deny = ["get_external_sdcard_path"]
@@ -22,6 +22,7 @@ Default permissions for the plugin
- `allow-get-safe-area-insets`
- `allow-get-screen-brightness`
- `allow-set-screen-brightness`
- `allow-get-external-sdcard-path`
- `allow-request-manage-storage-permission`
- `allow-checkPermissions`
- `allow-requestPermissions`
@@ -142,6 +143,32 @@ Denies the copy_uri_to_path command without any pre-configured scope.
<tr>
<td>
`native-bridge:allow-get-external-sdcard-path`
</td>
<td>
Enables the get_external_sdcard_path command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-get-external-sdcard-path`
</td>
<td>
Denies the get_external_sdcard_path command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-get-safe-area-insets`
</td>
@@ -19,6 +19,7 @@ permissions = [
"allow-get-safe-area-insets",
"allow-get-screen-brightness",
"allow-set-screen-brightness",
"allow-get-external-sdcard-path",
"allow-request-manage-storage-permission",
"allow-checkPermissions",
"allow-requestPermissions",
@@ -342,6 +342,18 @@
"const": "deny-copy-uri-to-path",
"markdownDescription": "Denies the copy_uri_to_path command without any pre-configured scope."
},
{
"description": "Enables the get_external_sdcard_path command without any pre-configured scope.",
"type": "string",
"const": "allow-get-external-sdcard-path",
"markdownDescription": "Enables the get_external_sdcard_path command without any pre-configured scope."
},
{
"description": "Denies the get_external_sdcard_path command without any pre-configured scope.",
"type": "string",
"const": "deny-get-external-sdcard-path",
"markdownDescription": "Denies the get_external_sdcard_path command without any pre-configured scope."
},
{
"description": "Enables the get_safe_area_insets command without any pre-configured scope.",
"type": "string",
@@ -547,10 +559,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`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`",
"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-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\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`\n- `allow-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
"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-get-screen-brightness`\n- `allow-set-screen-brightness`\n- `allow-get-external-sdcard-path`\n- `allow-request-manage-storage-permission`\n- `allow-checkPermissions`\n- `allow-requestPermissions`"
}
]
}
@@ -142,6 +142,13 @@ pub(crate) async fn set_screen_brightness<R: Runtime>(
app.native_bridge().set_screen_brightness(payload)
}
#[command]
pub(crate) async fn get_external_sdcard_path<R: Runtime>(
app: AppHandle<R>,
) -> Result<GetExternalSDCardPathResponse> {
app.native_bridge().get_external_sdcard_path()
}
#[command]
pub(crate) async fn request_manage_storage_permission<R: Runtime>(
app: AppHandle<R>,
@@ -118,6 +118,10 @@ impl<R: Runtime> NativeBridge<R> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn get_external_sdcard_path(&self) -> crate::Result<GetExternalSDCardPathResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn request_manage_storage_permission(
&self,
) -> crate::Result<RequestManageStoragePermissionResponse> {
@@ -55,6 +55,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
commands::get_safe_area_insets,
commands::get_screen_brightness,
commands::set_screen_brightness,
commands::get_external_sdcard_path,
commands::request_manage_storage_permission,
])
.setup(|app, api| {
@@ -189,6 +189,14 @@ impl<R: Runtime> NativeBridge<R> {
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn get_external_sdcard_path(&self) -> crate::Result<GetExternalSDCardPathResponse> {
self.0
.run_mobile_plugin("get_external_sdcard_path", ())
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn request_manage_storage_permission(
&self,
@@ -102,7 +102,7 @@ pub struct Product {
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Purchase {
pub platform: String, // "ios" or "android"
pub platform: String, // "ios" or "android"
pub package_name: Option<String>,
pub product_id: String,
pub transaction_id: Option<String>,
@@ -190,6 +190,13 @@ pub struct SetScreenBrightnessResponse {
pub error: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct GetExternalSDCardPathResponse {
pub path: Option<String>,
pub error: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RequestManageStoragePermissionResponse {
@@ -18,6 +18,7 @@ import { FileItem } from '@/types/system';
import { getDirPath } from '@/utils/path';
import { formatBytes } from '@/utils/book';
import { getOSPlatform } from '@/utils/misc';
import { getExternalSDCardPath } from '@/utils/bridge';
import { FILE_REVEAL_LABELS, FILE_REVEAL_PLATFORMS } from '@/utils/os';
import Dialog from '@/components/Dialog';
import Dropdown from '@/components/Dropdown';
@@ -101,14 +102,25 @@ 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 sdCardPathResponse = await getExternalSDCardPath();
let sdcardDirs = [
{ path: '/storage/emulated/0', label: '/sdcard/0' },
{ path: '/storage/emulated/0/Books', label: '/sdcard/0/Books' },
{ path: '/storage/emulated/0/Documents', label: '/sdcard/0/Documents' },
{ path: '/storage/emulated/0/Download', label: '/sdcard/0/Download' },
];
if (sdCardPathResponse.path) {
const externalSdCardPath = sdCardPathResponse.path;
sdcardDirs = [
...sdcardDirs,
{ path: externalSdCardPath, label: '/sdcard/1' },
{ path: `${externalSdCardPath}/Books`, label: '/sdcard/1/Books' },
{ path: `${externalSdCardPath}/Documents`, label: '/sdcard/1/Documents' },
{ path: `${externalSdCardPath}/Download`, label: '/sdcard/1/Download' },
];
}
const localDocumentDir = await documentDir();
setAndroidNewDirs([
// For Google Play version we won't request permission to access root of /sdcard
+12
View File
@@ -79,6 +79,11 @@ interface SetScreenBrightnessResponse {
error?: string;
}
interface GetExternalSDCardPathResponse {
path: string | null;
error?: string;
}
export async function copyURIToPath(request: CopyURIRequest): Promise<CopyURIResponse> {
const result = await invoke<CopyURIResponse>('plugin:native-bridge|copy_uri_to_path', {
payload: request,
@@ -167,6 +172,13 @@ export async function getScreenBrightness(): Promise<GetScreenBrightnessResponse
return result;
}
export async function getExternalSDCardPath(): Promise<GetExternalSDCardPathResponse> {
const result = await invoke<GetExternalSDCardPathResponse>(
'plugin:native-bridge|get_external_sdcard_path',
);
return result;
}
export async function setScreenBrightness(
request: SetScreenBrightnessRequest,
): Promise<SetScreenBrightnessResponse> {