You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android R中使用MediaStore API下载并保存图片/视频至共享文件夹的完整实现示例

完整实现:Android R(compileSdk 30)下通过MediaStore保存网络图片/视频到共享Downloads目录

正好适配你提到的场景——Android R环境下替代废弃的getExternalStoragePublicDirectory,用MediaStore API把网络资源存到共享的Downloads文件夹。下面是完整的可运行方案:

一、权限准备

首先在AndroidManifest.xml里配置必要权限:

<!-- 网络下载必须 -->
<uses-permission android:name="android.permission.INTERNET" />

<!-- 仅Android 10及以下需要写入权限,Android R+无需此权限 -->
<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="29" />

二、完整代码实现

我整理了一个工具类,包含网络下载、MediaStore写入的完整逻辑,同时处理了异常和流关闭的细节:

MediaStore文件保存工具类

import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;

public class MediaStoreSaveHelper {
    private static final String TAG = "MediaStoreSaveHelper";

    /**
     * 从网络下载文件并保存到Downloads共享目录
     * @param context 上下文
     * @param fileUrl 资源URL
     * @param fileName 保存的文件名(需带后缀,比如"bulldog.jpg")
     * @param mimeType 文件MIME类型,比如"image/jpeg"、"video/mp4"
     * @return 保存成功的Uri,失败返回null
     */
    public static Uri saveFileToDownloads(Context context, String fileUrl, String fileName, String mimeType) {
        ContentResolver resolver = context.getContentResolver();
        ContentValues contentValues = new ContentValues();

        // 设置文件基本信息
        contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimeType);
        // 指定保存到Downloads目录
        contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);

        // 根据文件类型选择对应的ContentUri(图片/视频通用Downloads的Uri也可以)
        Uri contentUri;
        if (mimeType.startsWith("image/")) {
            contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
        } else if (mimeType.startsWith("video/")) {
            contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
        } else {
            contentUri = MediaStore.Downloads.EXTERNAL_CONTENT_URI;
        }

        Uri uri = null;
        OutputStream outputStream = null;
        InputStream inputStream = null;
        HttpURLConnection connection = null;

        try {
            // 插入文件记录,获取可写入的Uri
            uri = resolver.insert(contentUri, contentValues);
            if (uri == null) {
                Log.e(TAG, "Failed to create new MediaStore record");
                return null;
            }

            // 打开输出流准备写入
            outputStream = resolver.openOutputStream(uri);
            if (outputStream == null) {
                Log.e(TAG, "Failed to open output stream for Uri: " + uri);
                return null;
            }

            // 发起网络请求获取输入流
            URL url = new URL(fileUrl);
            connection = (HttpURLConnection) url.openConnection();
            connection.setConnectTimeout(10000);
            connection.setReadTimeout(10000);

            if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                Log.e(TAG, "Server returned error code: " + connection.getResponseCode());
                return null;
            }

            inputStream = new BufferedInputStream(connection.getInputStream());
            // 流复制操作
            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
            outputStream.flush();
            Log.d(TAG, "File saved successfully to: " + uri);

        } catch (IOException e) {
            Log.e(TAG, "Error saving file", e);
            // 如果写入失败,删除已插入的空记录
            if (uri != null) {
                resolver.delete(uri, null, null);
                uri = null;
            }
        } finally {
            // 关闭所有流和连接
            try {
                if (outputStream != null) outputStream.close();
                if (inputStream != null) inputStream.close();
                if (connection != null) connection.disconnect();
            } catch (IOException e) {
                Log.e(TAG, "Error closing resources", e);
            }
        }

        return uri;
    }
}

在Activity中调用示例

注意:网络操作不能在主线程执行,这里用AsyncTask(如果是Java 11+推荐用ExecutorService,或者Kotlin协程):

import android.os.AsyncTask;
import android.os.Bundle;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;

public class SaveFileActivity extends AppCompatActivity {
    private static final String IMAGE_URL = "https://cdn.pixabay.com/photo/2020/04/21/06/41/bulldog-5071407_1280.jpg";

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

        // 触发保存操作(比如点击按钮时调用)
        new SaveFileTask().execute();
    }

    private class SaveFileTask extends AsyncTask<Void, Void, Uri> {
        @Override
        protected Uri doInBackground(Void... voids) {
            // 调用工具类保存图片到Downloads目录
            return MediaStoreSaveHelper.saveFileToDownloads(
                    SaveFileActivity.this,
                    IMAGE_URL,
                    "bulldog_download.jpg", // 自定义文件名
                    "image/jpeg" // MIME类型
            );
        }

        @Override
        protected void onPostExecute(Uri uri) {
            super.onPostExecute(uri);
            if (uri != null) {
                Toast.makeText(SaveFileActivity.this, "图片已保存到Downloads", Toast.LENGTH_LONG).show();
            } else {
                Toast.makeText(SaveFileActivity.this, "保存失败", Toast.LENGTH_SHORT).show();
            }
        }
    }
}

三、关键细节说明

  • 文件类型适配:代码根据MIME类型自动选择对应的ContentUri,确保图片/视频都能被系统媒体库正确识别
  • 异常处理:如果写入失败,会自动删除MediaStore中插入的空记录,避免产生无效文件
  • 权限兼容:针对Android 10及以下保留了WRITE_EXTERNAL_STORAGE权限配置,Android R+无需此权限即可写入共享目录
  • 线程安全:网络操作和文件写入都在后台线程执行,避免阻塞主线程导致ANR

内容的提问来源于stack exchange,提问作者Kanaiya Katarmal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 14:12:44