如何在 API 29 或 Android Q 中使用 DownloadManager 下载文件?

Bis*_*iaz 29 android android-download-manager

由于我是 Android 开发的新手,我正在尝试使用 DownloadManager 来简化应用程序。

这是代码

public class MainActivity extends AppCompatActivity implements ActivityCompat.OnRequestPermissionsResultCallback{

Button btn;

private long referenceID;
private DownloadManager downloadManager;
private static final int PERMISSION_REQUEST_CODE = 1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    btn = findViewById(R.id.btn);

    btn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {



            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M){


                if (checkPermission())
                {

                    /*** If Storage Permission Is Given, Check External storage is available for read and write***/

                    Uri image_uri = Uri.parse("https://unifiedclothes.com/Unifiedclothes/App_Gallery/thumb_8_121432471036-1432471036-SC-505.jpg");

                    referenceID = DownloadImage(image_uri);




                } else {

                    requestPermission();
                }

            }

            else{
                Toast.makeText(MainActivity.this,"Permission Is Granted..",Toast.LENGTH_SHORT).show();

            }
        }
    });

    registerReceiver(receiver,new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}

private BroadcastReceiver receiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {

        String action = intent.getAction();

        if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)){



            DownloadManager.Query ImageDownloadQuery = new DownloadManager.Query();
            //set the query filter to our previously Enqueued download
            ImageDownloadQuery.setFilterById(referenceID);

            //Query the download manager about downloads that have been requested.
            Cursor cursor = downloadManager.query(ImageDownloadQuery);

            if(cursor.moveToFirst()){

                Toast.makeText(MainActivity.this,DownloadStatus(cursor),Toast.LENGTH_SHORT).show();
            }



        }

    }
};

private String DownloadStatus(Cursor cursor){

    //column for download  status
    int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS);
    int status = cursor.getInt(columnIndex);
    //column for reason code if the download failed or paused
    int columnReason = cursor.getColumnIndex(DownloadManager.COLUMN_REASON);
    int reason = cursor.getInt(columnReason);



    String statusText = "";
    String reasonText = "";

    switch(status){
        case DownloadManager.STATUS_FAILED:
            statusText = "STATUS_FAILED";
            switch(reason){
                case DownloadManager.ERROR_CANNOT_RESUME:
                    reasonText = "ERROR_CANNOT_RESUME";
                    break;
                case DownloadManager.ERROR_DEVICE_NOT_FOUND:
                    reasonText = "ERROR_DEVICE_NOT_FOUND";
                    break;
                case DownloadManager.ERROR_FILE_ALREADY_EXISTS:
                    reasonText = "ERROR_FILE_ALREADY_EXISTS";
                    break;
                case DownloadManager.ERROR_FILE_ERROR:
                    reasonText = "ERROR_FILE_ERROR";
                    break;
                case DownloadManager.ERROR_HTTP_DATA_ERROR:
                    reasonText = "ERROR_HTTP_DATA_ERROR";
                    break;
                case DownloadManager.ERROR_INSUFFICIENT_SPACE:
                    reasonText = "ERROR_INSUFFICIENT_SPACE";
                    break;
                case DownloadManager.ERROR_TOO_MANY_REDIRECTS:
                    reasonText = "ERROR_TOO_MANY_REDIRECTS";
                    break;
                case DownloadManager.ERROR_UNHANDLED_HTTP_CODE:
                    reasonText = "ERROR_UNHANDLED_HTTP_CODE";
                    break;
                case DownloadManager.ERROR_UNKNOWN:
                    reasonText = "ERROR_UNKNOWN";
                    break;
            }
            break;
        case DownloadManager.STATUS_PAUSED:
            statusText = "STATUS_PAUSED";
            switch(reason){
                case DownloadManager.PAUSED_QUEUED_FOR_WIFI:
                    reasonText = "PAUSED_QUEUED_FOR_WIFI";
                    break;
                case DownloadManager.PAUSED_UNKNOWN:
                    reasonText = "PAUSED_UNKNOWN";
                    break;
                case DownloadManager.PAUSED_WAITING_FOR_NETWORK:
                    reasonText = "PAUSED_WAITING_FOR_NETWORK";
                    break;
                case DownloadManager.PAUSED_WAITING_TO_RETRY:
                    reasonText = "PAUSED_WAITING_TO_RETRY";
                    break;
            }
            break;
        case DownloadManager.STATUS_PENDING:
            statusText = "STATUS_PENDING";
            break;
        case DownloadManager.STATUS_SUCCESSFUL:
            statusText = "Image Saved Successfully";
            //reasonText = "Filename:\n" + filename;
            Toast.makeText(MainActivity.this, "Download Status:" + "\n" + statusText + "\n" + reasonText, Toast.LENGTH_SHORT).show();
            break;
    }

    return statusText + reasonText;


}


private long DownloadImage(Uri uri){

    long downloadReference;

    downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);

    DownloadManager.Request request = new DownloadManager.Request(uri);
    //Setting title of request
    request.setTitle("Image Download");

    //Setting description of request
    request.setDescription("Image download using DownloadManager.");


    request.setDestinationInExternalPublicDir(getExternalFilesDir(Environment.DIRECTORY_PICTURES) + "/NewFile","sample2.jpg");
    //request.allowScanningByMediaScanner();
    request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
    downloadReference = downloadManager.enqueue(request);


    return  downloadReference;
}


private boolean checkPermission() {
    int result = ContextCompat.checkSelfPermission(MainActivity.this, android.Manifest.permission.WRITE_EXTERNAL_STORAGE);
    if (result == PackageManager.PERMISSION_GRANTED) {
        return true;
    } else {
        return false;
    }
}


private void requestPermission() {

    ActivityCompat.requestPermissions(MainActivity.this, new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, PERMISSION_REQUEST_CODE);
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);

    if (requestCode == PERMISSION_REQUEST_CODE) {

        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {

            Uri image_uri = Uri.parse("https://www.dccomics.com/sites/default/files/Char_GetToKnow_Batman80_5ca54cb83a27a6.53173051.png");

            referenceID = DownloadImage(image_uri);


        }

        else {

            Toast.makeText(MainActivity.this, "Permission Denied... \n You Should Allow External Storage Permission To Download Images.", Toast.LENGTH_LONG).show();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

}

当我在低于 API 29 的任何设备上运行它时,它运行良好(我的测试设备是 Nexus 5X,ApI 28 模拟器)。但是当我在 Nexus 5X 上运行它时,API 29 应用程序崩溃了。这是日志:

2019-09-24 20:51:46.354 11322-11344/? E/DatabaseUtils: Writing exception to parcel
java.lang.IllegalStateException: Not one of standard directories: /storage/emulated/0/Android/data/com.blz.prisoner.downloadmanager/files/Pictures/NewFile
    at com.android.providers.downloads.DownloadProvider.call(DownloadProvider.java:651)
    at android.content.ContentProvider.call(ContentProvider.java:2152)
    at android.content.ContentProvider$Transport.call(ContentProvider.java:477)
    at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:277)
    at android.os.Binder.execTransactInternal(Binder.java:1021)
    at android.os.Binder.execTransact(Binder.java:994)
Run Code Online (Sandbox Code Playgroud)

2019-09-24 20:51:46.355 15023-15023/com.blz.prisoner.downloadmanager D/AndroidRuntime:关闭虚拟机

--------- beginning of crash
2019-09-24 20:51:46.360 15023-15023/com.blz.prisoner.downloadmanager E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.blz.prisoner.downloadmanager, PID: 15023
    java.lang.IllegalStateException: Not one of standard directories: /storage/emulated/0/Android/data/com.blz.prisoner.downloadmanager/files/Pictures/NewFile
        at android.os.Parcel.createException(Parcel.java:2079)
        at android.os.Parcel.readException(Parcel.java:2039)
        at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:188)
        at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:140)
        at android.content.ContentProviderProxy.call(ContentProviderNative.java:658)
        at android.content.ContentProviderClient.call(ContentProviderClient.java:558)
        at android.content.ContentProviderClient.call(ContentProviderClient.java:546)
        at android.app.DownloadManager$Request.setDestinationInExternalPublicDir(DownloadManager.java:567)
        at com.blz.prisoner.downloadmanager.MainActivity.DownloadImage(MainActivity.java:206)
        at com.blz.prisoner.downloadmanager.MainActivity.access$200(MainActivity.java:29)
        at com.blz.prisoner.downloadmanager.MainActivity$1.onClick(MainActivity.java:60)
        at android.view.View.performClick(View.java:7140)
        at android.view.View.performClickInternal(View.java:7117)
        at android.view.View.access$3500(View.java:801)
        at android.view.View$PerformClick.run(View.java:27351)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
Run Code Online (Sandbox Code Playgroud)

我认为问题出在DownloadImage(Uri uri)函数中的“ request.setDestinationInExternalPublicDir(getExternalFilesDir(Environment.DIRECTORY_PICTURES) + "/NewFile","sample2.jpg");中。如何解决问题??

另一个问题是,当我在低于 API 29 的设备上运行该应用程序时,它运行良好,但是当我在完成下载后单击通知时,它不会在图库/保存的文件夹中打开图像。

Z3R*_*3R0 13

我只是通过使用解决了:

setDestinationInExternalFilesDir(context, relativePath, filename);
Run Code Online (Sandbox Code Playgroud)

代替:

setDestinationInExternalPublicDir(relativePath, filename);
Run Code Online (Sandbox Code Playgroud)

我的相对路径是:

Environment.getExternalStorageDirectory().getPath() + "MyExternalStorageAppPath"
Run Code Online (Sandbox Code Playgroud)

我的清单中也有:

android:requestLegacyExternalStorage="true"
Run Code Online (Sandbox Code Playgroud)

使用旧存储管理(共享存储)而不是从 Android 10 及更高版本使用的新存储管理(范围存储)。

请记住,通过使用“setDestinationInExternalFilesDir”文件将被下载到专用于您的应用程序的外部存储器,因此:“external/Android/data/your_app_name/path_you_used_on_function”。如果要将其下载到其他位置,则需要在使用输入和输出流下载它后移动它。要使用 Android 10 或更高版本中的其他应用程序打开文件,您必须使用 FileProvider。

如果有人需要,这是移动的代码(移动,而不是复制。所以原始文件将被删除。删除“source.delete();”如果要复制文件而不是删除源文件)一个文件从一个位置到另一个:

public static boolean moveFile(File source, String destPath){
        if(source.exists()){
            File dest = new File(destPath);
            checkMakeDirs(dest.getParent());
            try (FileInputStream fis = new FileInputStream(source);
                 FileOutputStream fos = new FileOutputStream(dest)){
                if(!dest.exists()){
                    dest.createNewFile();
                }
                writeToOutputStream(fis, fos);
                source.delete();
                return true;
            } catch (IOException ioE){
                Log.e(TAG, ioE.getMessage());
            }
        }
        return false;
    }

private static void writeToOutputStream(InputStream is, OutputStream os) throws IOException {
        byte[] buffer = new byte[1024];
        int length;
        if (is != null) {
            while ((length = is.read(buffer)) > 0x0) {
                os.write(buffer, 0x0, length);
            }
        }
        os.flush();
    }
Run Code Online (Sandbox Code Playgroud)

用法(“源”是您需要移动的文件,“路径”是目标):

if(FilesUtils.moveFile(source, path)) {
     // Success Moving File, do what you need with it
}
Run Code Online (Sandbox Code Playgroud)

DownloadManager 完成时的广播接收器:

private static class DownloadFileReceiver extends BroadcastReceiver {

        private DownloadManager mDownloadManager;
        private String mPath;

        private DownloadFileReceiver(DownloadManager dManager, String path){
            mDownloadManager = dManager;
            mPath = path;
        }

        /** Override BroadcastReceiver Methods **/
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
                Bundle extras = intent.getExtras();
                DownloadManager.Query q = new DownloadManager.Query();
                q.setFilterById(extras.getLong(DownloadManager.EXTRA_DOWNLOAD_ID));
                Cursor c = mDownloadManager.query(q);
                if (c.moveToFirst()) {
                    int status = c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS));
                    if (status == DownloadManager.STATUS_SUCCESSFUL) {
                        String fullPath = null; File source = null;
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                            fullPath = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI));
                            source = new File(Uri.parse(fullPath).getPath());
                        } else {
                            fullPath = c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME));
                            source = new File(fullPath);
                        }
                    }
                }
                c.close();
            }
            Objects.requireNonNull(context).unregisterReceiver(this);
        }
    }
Run Code Online (Sandbox Code Playgroud)

将其注册到 DownloadManager 实例:

context.registerReceiver(new DownloadFileReceiver(downloadManager, path),
                new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
Run Code Online (Sandbox Code Playgroud)

checkMakeDirs(此检查目录是否存在或是否可以成功创建)和 makeDirs(无需检查即可创建)代码:

public static boolean checkMakeDirs(String dirPath){
        try {
            File dir = new File(dirPath);
            return dir.exists() || dir.mkdirs();
        } catch (Exception e) {
            Log.e(TAG, e.getMessage());
        }
        return false;
    }

    public static void makeDirs(String dirPath){
        try {
            File dir = new File(dirPath);
            if(!dir.exists()){
                dir.mkdirs();
            }
        } catch (Exception e){
            Log.e(TAG, e.getMessage());
        }
    }
Run Code Online (Sandbox Code Playgroud)

重要提示:
从 2021 年 5 月 7 日起,如果您的应用在 Google Play 商店中,理论上您必须 targetSdk=30 并且您必须仅使用 Scoped Storage 来访问您的文件(因此仅使用您的应用的特定于应用的目录) . 这意味着您需要使用:

context.getFilesDir();


thi*_* Kj 8

要使用下载管理器在 Android Q 及以下版本中下载文件:

如果您的目标是 Android Q(29),则无需选择退出范围存储。(android:requestLegacyExternalStorage="true"不需要)

清单文件

<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER" />
Run Code Online (Sandbox Code Playgroud)

代码 :

 private fun onDownload() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        downloadFile("xxx.jpg", "File Desc", "url")
    }else{
        val result = requestRuntimePermission(
            this,
            Manifest.permission.WRITE_EXTERNAL_STORAGE
        )
        result.success {
             downloadFile("xxx.jpg", "File Desc", "url")
        }
    }

}

private fun downloadFile(fileName : String, desc :String, url : String){
    // fileName -> fileName with extension
    val request = DownloadManager.Request(Uri.parse(url))
        .setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
        .setTitle(fileName)
        .setDescription(desc)
        .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
        .setAllowedOverMetered(true)
        .setAllowedOverRoaming(false)
        .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS,fileName)
    val downloadManager= getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
    val downloadID = downloadManager.enqueue(request)
}
Run Code Online (Sandbox Code Playgroud)

将文件存储在外部应用程序特定目录中

.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_MUSIC,fileName)
Run Code Online (Sandbox Code Playgroud)

将文件存储在外部公共目录中

.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS,fileName)
Run Code Online (Sandbox Code Playgroud)

  • 您从哪里获得权限`“android.permission.ACCESS_DOWNLOAD_MANAGER”`?我找不到任何相关信息 (3认同)

小智 2

我相信问题出在你提到的那一行。您正在设置的串联正在将文件的目录更改为非标准目录。

request.setDestinationInExternalPublicDir(getExternalFilesDir(Environment.DIRECTORY_PICTURES) + "/NewFile","sample2.jpg")
Run Code Online (Sandbox Code Playgroud)

错误表明。查看该行告诉您它正在尝试保存到哪个目录。

/storage/emulated/0/Android/data/com.blz.prisoner.downloadmanager/files/Pictures/NewFile
Run Code Online (Sandbox Code Playgroud)

因此,当您这样做时,它会尝试另存为:

/storage/emulated/0/Android/data/com.blz.prisoner.downloadmanager/files/Pictures/NewFile/sample2.jpg
Run Code Online (Sandbox Code Playgroud)

解决方案是将文件保存到标准目录。为此,您只需要删除串联即可。

request.setDestinationInExternalPublicDir(getExternalFilesDir(Environment.DIRECTORY_PICTURES),"sample2.jpg")
Run Code Online (Sandbox Code Playgroud)

然后它会尝试保存到标准目录

/storage/emulated/0/Android/data/com.blz.prisoner.downloadmanager/files/Pictures/sample2.jpg
Run Code Online (Sandbox Code Playgroud)