Android平台下如何修改现有代码实现文件下载至可移除USB存储设备?
I see you're looking to switch your download target from internal storage to a removable USB drive. Let's break down the changes you need to make, covering path handling, permissions, and full code adjustments.
1. Adjust the Download Path
Your current code uses a hardcoded internal storage path (/sdcard/fastDl). To target the USB drive, you have two options:
- Hardcoded approach (for quick testing): Replace your existing path with your specific USB directory:
val downloadPath = "/storage/08E7-324HI/Downloads" - Dynamic path detection (better for production): USB volume IDs vary by device, so use this helper function to automatically find mounted removable USB drives:
Use this function inprivate fun getRemovableUsbPath(): String? { val storageManager = getSystemService(STORAGE_SERVICE) as StorageManager val storageVolumes = storageManager.storageVolumes for (volume in storageVolumes) { // Check if the volume is removable and accessible if (volume.isRemovable && volume.directory != null) { return volume.directory?.absolutePath + "/Downloads" } } return null // No USB drive found }downloadStart()with a fallback to internal storage:val downloadPath = getRemovableUsbPath() ?: run { txtPercent.text = "USB drive not found, falling back to internal storage" this.getExternalFilesDir("fastDownload")!!.absolutePath }
2. Update Permissions for Android 10+
Your current WRITE_EXTERNAL_STORAGE permission is deprecated on Android 10 (API 29) and above. Here's how to adapt:
First, update your AndroidManifest.xml to include the necessary permissions:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
Then modify your permission check and request logic:
Replace checkPermission():
private fun checkPermission(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // For Android 11+, check if we have full storage access Environment.isExternalStorageManager() } else { // For older versions, use the legacy permission ContextCompat.checkSelfPermission(applicationContext, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED } }
Replace requestPermission():
private fun requestPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { // Launch system settings to request full storage access val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) intent.data = Uri.parse("package:$packageName") startActivityForResult(intent, PERMISSION_REQUEST_CODE) } else { // Request legacy write permission for older Android versions ActivityCompat.requestPermissions( this, arrayOf(WRITE_EXTERNAL_STORAGE), PERMISSION_REQUEST_CODE ) } }
Update onRequestPermissionsResult() and add onActivityResult() to handle the Android 11+ permission flow:
override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) when (requestCode) { PERMISSION_REQUEST_CODE -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Environment.isExternalStorageManager()) { Snackbar.make(window.decorView.rootView, "Storage access granted", Snackbar.LENGTH_LONG).show() downloadStart() } else { Snackbar.make(window.decorView.rootView, "Storage access denied", Snackbar.LENGTH_LONG).show() } } else { val writeAccepted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED if (writeAccepted) { Snackbar.make(window.decorView.rootView, "Storage access granted", Snackbar.LENGTH_LONG).show() downloadStart() } else { Snackbar.make(window.decorView.rootView, "Storage access denied", Snackbar.LENGTH_LONG).show() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { showMessageOKCancel("You need storage access to download files") { _, _ -> requestPermissions(arrayOf(WRITE_EXTERNAL_STORAGE), PERMISSION_REQUEST_CODE) } } } } } } } // Handle result of the Android 11+ storage permission intent override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == PERMISSION_REQUEST_CODE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Environment.isExternalStorageManager()) { downloadStart() } else { Snackbar.make(window.decorView.rootView, "Storage access denied", Snackbar.LENGTH_LONG).show() } } } }
3. Full Modified Code
Here's the complete adjusted MainActivity.kt with all changes:
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.content.Intent import android.content.DialogInterface import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.os.Environment import android.provider.Settings import android.net.Uri import android.os.storage.StorageManager import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import com.aghamiri.fastdl.DownloadManager import com.aghamiri.fastdl.OnDownloadProgressListener import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.activity_main.* class MainActivity : AppCompatActivity() { private val PERMISSION_REQUEST_CODE = 200 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) btn.setOnClickListener { if (!checkPermission()) { requestPermission() } else { downloadStart() } } } private fun downloadStart() { val downloadLink = "http://server.info/news.txt" val downloadPath = getRemovableUsbPath() ?: run { txtPercent.text = "USB drive not found, falling back to internal storage" this.getExternalFilesDir("fastDownload")!!.absolutePath } val dlManager = DownloadManager(downloadLink, downloadPath, object : OnDownloadProgressListener { override fun downloadStart() { logi("downloadStart") } override fun downloadedSuccess() { logi("downloadedSuccess") txtPercent.text = "Download successful to: $downloadPath" } override fun downloadCancel() { loge("downloadCancel") } override fun downloadFail(error: String?) { loge("downloadFail:$error") txtPercent.text = "Download failed: $error" } override fun percent(percent: Int) { runOnUiThread { progressBar.progress = percent txtPercent.text = "$percent%" logd("downloadProgress:$percent") } } }) dlManager.execute() } private fun getRemovableUsbPath(): String? { val storageManager = getSystemService(STORAGE_SERVICE) as StorageManager val storageVolumes = storageManager.storageVolumes for (volume in storageVolumes) { if (volume.isRemovable && volume.directory != null) { return volume.directory?.absolutePath + "/Downloads" } } return null } private fun checkPermission(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { Environment.isExternalStorageManager() } else { ContextCompat.checkSelfPermission(applicationContext, WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED } } private fun requestPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) intent.data = Uri.parse("package:$packageName") startActivityForResult(intent, PERMISSION_REQUEST_CODE) } else { ActivityCompat.requestPermissions( this, arrayOf(WRITE_EXTERNAL_STORAGE), PERMISSION_REQUEST_CODE ) } } private fun showMessageOKCancel(message: String, okListener: DialogInterface.OnClickListener) { AlertDialog.Builder(this@MainActivity) .setMessage(message) .setPositiveButton("Agree", okListener) .setNegativeButton("Cancel", null) .create() .show() } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) when (requestCode) { PERMISSION_REQUEST_CODE -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Environment.isExternalStorageManager()) { Snackbar.make(window.decorView.rootView, "Storage access granted", Snackbar.LENGTH_LONG).show() downloadStart() } else { Snackbar.make(window.decorView.rootView, "Storage access denied", Snackbar.LENGTH_LONG).show() } } else { val writeAccepted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED if (writeAccepted) { Snackbar.make(window.decorView.rootView, "Storage access granted", Snackbar.LENGTH_LONG).show() downloadStart() } else { Snackbar.make(window.decorView.rootView, "Storage access denied", Snackbar.LENGTH_LONG).show() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && shouldShowRequestPermissionRationale(WRITE_EXTERNAL_STORAGE)) { showMessageOKCancel("You need storage access to download files") { _, _ -> requestPermissions(arrayOf(WRITE_EXTERNAL_STORAGE), PERMISSION_REQUEST_CODE) } } } } } } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == PERMISSION_REQUEST_CODE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { if (Environment.isExternalStorageManager()) { downloadStart() } else { Snackbar.make(window.decorView.rootView, "Storage access denied", Snackbar.LENGTH_LONG).show() } } } } // Helper log functions (replace with Android's Log class if you don't have these) private fun logi(message: String) { android.util.Log.i("DownloadLog", message) } private fun loge(message: String) { android.util.Log.e("DownloadLog", message) } private fun logd(message: String) { android.util.Log.d("DownloadLog", message) } }
4. Key Notes
- USB Mount Status: Ensure the USB drive is properly mounted before testing. The helper function will return
nullif no removable USB is detected. - Google Play Policy: The
MANAGE_EXTERNAL_STORAGEpermission is restricted for most apps. For Play Store publication, consider using the Storage Access Framework (SAF) to let users manually select the USB folder, which is more policy-compliant. - Path Variability: USB paths differ across devices (e.g.,
/storage/XXXX-XXXXwhere XXXX is a volume ID), so dynamic detection is preferred over hardcoding for production apps.
内容的提问来源于stack exchange,提问作者Arge

