diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/FileChooserHelper.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/FileChooserHelper.kt index 700330c5..a023e089 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/helper/FileChooserHelper.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/helper/FileChooserHelper.kt @@ -12,12 +12,12 @@ import com.v2ray.ang.R import com.v2ray.ang.extension.toast /** - * Helper for choosing files using ACTION_GET_CONTENT intent. - * + * Helper for choosing and creating files using Android Storage Access Framework. + * Supports both file selection (ACTION_GET_CONTENT) and file creation (CreateDocument). */ - class FileChooserHelper(private val activity: AppCompatActivity) { private var fileChooserCallback: ((Uri?) -> Unit)? = null + private var documentCreateCallback: ((Uri?) -> Unit)? = null private val fileChooserLauncher: ActivityResultLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> @@ -30,8 +30,14 @@ class FileChooserHelper(private val activity: AppCompatActivity) { fileChooserCallback = null } + private val documentCreateLauncher: ActivityResultLauncher = + activity.registerForActivityResult(ActivityResultContracts.CreateDocument("application/zip")) { uri -> + documentCreateCallback?.invoke(uri) + documentCreateCallback = null + } + /** - * Launch file chooser with ACTION_GET_CONTENT intent. + * Launch file chooser with ACTION_GET_CONTENT intent to select an existing file. * * @param mimeType MIME type filter for files * @param onResult Callback invoked with the selected file URI (null if cancelled) @@ -58,4 +64,25 @@ class FileChooserHelper(private val activity: AppCompatActivity) { fileChooserCallback = null } } + + /** + * Launch document creator to create a new file at user-selected location. + * + * @param fileName Default file name for the new document + * @param onResult Callback invoked with the created file URI (null if cancelled) + */ + fun createDocument( + fileName: String, + onResult: (Uri?) -> Unit + ) { + documentCreateCallback = onResult + try { + documentCreateLauncher.launch(fileName) + } catch (ex: ActivityNotFoundException) { + Log.e(AppConfig.TAG, "Document creator activity not found", ex) + activity.toast(R.string.toast_require_file_manager) + documentCreateCallback?.invoke(null) + documentCreateCallback = null + } + } } \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt index dbacb324..77fd5d06 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BackupActivity.kt @@ -4,7 +4,6 @@ import android.app.AlertDialog import android.content.Intent import android.os.Bundle import android.util.Log -import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.FileProvider import androidx.lifecycle.lifecycleScope import com.tencent.mmkv.MMKV @@ -20,7 +19,6 @@ import com.v2ray.ang.extension.toastSuccess import com.v2ray.ang.handler.MmkvManager import com.v2ray.ang.handler.SettingsChangeManager import com.v2ray.ang.handler.WebDavManager -import com.v2ray.ang.dto.PermissionType import com.v2ray.ang.util.ZipUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -36,30 +34,6 @@ class BackupActivity : BaseActivity() { resources.getStringArray(R.array.config_backup_options) } - private val createBackupFile = - registerForActivityResult(ActivityResultContracts.CreateDocument("application/zip")) { uri -> - if (uri != null) { - try { - val ret = backupConfigurationToCache() - if (ret.first) { - // Copy the cached zip file to user-selected location - contentResolver.openOutputStream(uri)?.use { output -> - File(ret.second).inputStream().use { input -> - input.copyTo(output) - } - } - // Clean up cache file - File(ret.second).delete() - toastSuccess(R.string.toast_success) - } else { - toastError(R.string.toast_failure) - } - } catch (e: Exception) { - Log.e(AppConfig.TAG, "Failed to backup configuration", e) - toastError(R.string.toast_failure) - } - } - } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -184,7 +158,30 @@ class BackupActivity : BaseActivity() { Locale.getDefault() ).format(System.currentTimeMillis()) val defaultFileName = "${getString(R.string.app_name)}_${dateFormatted}.zip" - createBackupFile.launch(defaultFileName) + + launchCreateDocument(defaultFileName) { uri -> + if (uri != null) { + try { + val ret = backupConfigurationToCache() + if (ret.first) { + // Copy the cached zip file to user-selected location + contentResolver.openOutputStream(uri)?.use { output -> + File(ret.second).inputStream().use { input -> + input.copyTo(output) + } + } + // Clean up cache file + File(ret.second).delete() + toastSuccess(R.string.toast_success) + } else { + toastError(R.string.toast_failure) + } + } catch (e: Exception) { + Log.e(AppConfig.TAG, "Failed to backup configuration", e) + toastError(R.string.toast_failure) + } + } + } } private fun restoreViaLocal() { diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt index 2c66f699..c7117a11 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/BaseActivity.kt @@ -255,6 +255,21 @@ abstract class BaseActivity : AppCompatActivity() { } } + /** + * Launch document creator to create a new file at user-selected location. + * Convenience method that delegates to fileChooser helper. + * Note: No permission check needed as CreateDocument uses Storage Access Framework. + * + * @param fileName Default file name for the new document + * @param onResult Callback invoked with the created file URI (null if cancelled) + */ + protected fun launchCreateDocument( + fileName: String, + onResult: (Uri?) -> Unit + ) { + fileChooser.createDocument(fileName, onResult) + } + /** * Launch QR code scanner with camera permission check. * Convenience method that delegates to qrCodeScanner helper.