DownloadManager не работает на Android 10 (Q)
Я долго боролся с этой проблемой... Я обновляю приложение, которое использует DownloadManger для выполнения простой задачи, такой как загрузка файла в общедоступный каталог внешнего хранилища, то есть:
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
Здесь все отлично работает с Android api 19-28. Проблемы возникают при тестировании по API 29 (Q/10). Android внедрено хранения и область видимости, так осуждал getExternalStoragePublicDirectory... В итоге мне нужно выяснить, совместимое решение для поддержки API, 19-29. Я не могу использовать внутреннее хранилище приложения, так как DownloadManager выдаст исключение SecurityException. В документации Android указано, что я могу использовать DownloadManager.Request.setDestinationUri
и даже упоминается для Android Q, что я могу использовать Context.getExternalFilesDir(String)
. Когда я это делаю, путь по-прежнему остается эмулированным путем:
/storage/emulated/0/Android/data/com.my.package.name/files/Download/myFile.xml
Я получаю обратный вызов от диспетчера загрузки, что загрузка завершена (с правильным идентификатором), но затем я не могу получить загрузку из области, в которой я ее сохранил. Я проверяю, существует ли файл, и он возвращает false:
new File("/storage/emulated/0/Android/data/com.my.package.name/files/Download/myFile.xml").exists();
Любая помощь приветствуется
Добавление кода для контекста. Итак, настройка менеджера загрузок
private void startDownload() {
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
registerReceiver(downloadReceiver, filter);
String remoteURL= getString(R.string.remote_url);
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(remoteUrl));
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE);
request.setTitle(getString(R.string.download_title));
request.setDescription(getString(R.string.download_description));
request.setDestinationUri(Uri.fromFile(new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "myFile.xml")));
DownloadManager manager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
mainDownloadID= manager.enqueue(request);
}
проверка файла, если он существует:
new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "myFile.xml").exists(); //this returns false in the onReceive (and download IDs match)
4 ответа
Попробуйте добавить это в свой файл манифеста в теге приложения
android:requestLegacyExternalStorage="true"
Да, его объем хранилища, но даже если вы можете загрузить файл в Q + с помощью downloadmanger, делать это не нужно.
android:requestLegacyExternalStorage="true"
Я так и делаю.
- манифест
- ->
Downloadmanger.
val fileName = Constants.FILE_NAME + Date().time val downloadUri = Uri.parse(media.url) val request = DownloadManager.Request( downloadUri ) request.setAllowedNetworkTypes( DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE ) .setAllowedOverRoaming(true).setTitle("Some name") .setDescription("Downloading file") .setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS, File.separator + FOLDER + File.separator + fileName ) Toast.makeText( context, "Download successfully to ${downloadUri?.path}", Toast.LENGTH_LONG ).show() downloadManager.enqueue(request)
Следовательно, он будет запрашивать разрешение на запись ниже Q, но в Q и Q + он будет загружаться без запроса разрешения в / Download / folder dir.
Пути к файлам вне частных каталогов приложения в Android Q и выше бесполезны.
См. https://developer.android.com/training/data-storage.
Вам также нужно спросить пользователя, где скачать файлы, это даст вам URI для DownloadManager
пункт назначения.
https://developer.android.com/training/data-storage/shared/documents-files
Вы, вероятно, захотите сохранить это разрешение
https://developer.android.com/training/data-storage/shared/documents-files
Используйте этот код и наслаждайтесь, этот код использует RxJava для сетевого вызова:
import android.content.ContentValues
import android.content.Context
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import io.reactivex.Observable
import io.reactivex.ObservableEmitter
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.ResponseBody
import java.io.*
import java.net.HttpURLConnection
import java.util.concurrent.TimeUnit
class FileDownloader(
private val context: Context,
private val url: String,
private val fileName: String
) {
private val okHttpClient: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(60, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.build()
private val errorMessage = "File couldn't be downloaded"
private val bufferLengthBytes: Int = 1024 * 4
fun download(): Observable<Int> {
return Observable.create<Int> { emitter ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // To Download File for Android 10 and above
val content = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val uri = context.contentResolver.insert(
MediaStore.Downloads.EXTERNAL_CONTENT_URI,
content
)
uri?.apply {
val responseBody = getResponseBody(url)
if (responseBody != null
) {
responseBody.byteStream().use { inputStream ->
context.contentResolver.openOutputStream(uri)?.use { fileOutStream ->
writeOutStream(
inStream = inputStream,
outStream = fileOutStream,
contentLength = responseBody.contentLength(),
emitter = emitter
)
}
emitter.onComplete()
}
} else {
emitter.onError(Throwable(errorMessage))
}
}
}
else { // For Android versions below than 10
val directory = File(
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS).absolutePath
).apply {
if (!exists()) {
mkdir()
}
}
val file = File(directory, fileName)
val responseBody = getResponseBody(url)
if (responseBody != null) {
responseBody.byteStream().use { inputStream ->
file.outputStream().use { fileOutStream ->
writeOutStream(
inStream = inputStream,
outStream = fileOutStream,
contentLength = responseBody.contentLength(),
emitter = emitter
)
}
emitter.onComplete()
}
} else {
emitter.onError(Throwable(errorMessage))
}
}
}
}
private fun getResponseBody(url: String): ResponseBody? {
val response = okHttpClient.newCall(Request.Builder().url(url).build()).execute()
return if (response.code >= HttpURLConnection.HTTP_OK &&
response.code < HttpURLConnection.HTTP_MULT_CHOICE &&
response.body != null
)
response.body
else
null
}
private fun writeOutStream(
inStream: InputStream,
outStream: OutputStream,
contentLength: Long,
emitter: ObservableEmitter<Int>) {
var bytesCopied = 0
val buffer = ByteArray(bufferLengthBytes)
var bytes = inStream.read(buffer)
while (bytes >= 0) {
outStream.write(buffer, 0, bytes)
bytesCopied += bytes
bytes = inStream.read(buffer)
// emitter.onNext(
((bytesCopied * 100) / contentLength).toInt()
// )
}
outStream.flush()
outStream.close()
}
}
На вызывающей стороне вы должны исправить это:
private fun downloadFileFromUrl(context: Context, url: String, fileName: String) {
FileDownloader(
context = context,
url = url,
fileName = fileName
).download()
.throttleFirst(2, TimeUnit.SECONDS)
.toFlowable(BackpressureStrategy.LATEST)
.subscribeOn(Schedulers.io())
.observeOn(mainThread())
.subscribe({
// onNext: Downloading in progress
}, { error ->
// onError: Download Error
requireContext()?.apply {
Toast.makeText(this, error.message, Toast.LENGTH_SHORT).show()
}
}, {
// onComplete: Download Complete
requireContext()?.apply {
Toast.makeText(this, "File downloaded to Downloads Folder", Toast.LENGTH_SHORT).show()
}
})
}