Android 10及以上版本无法保存应用生成图片至图库求助
Hey there! Let's get your avatar saving working on newer Android versions. The root problem here is scoped storage—introduced in Android 10 (API 29)—which restricts direct access to shared external storage directories like the ones you're using with Environment.getExternalStorageDirectory(). Your current code works on Android 7 because it doesn't enforce these restrictions, but we need to adjust it to comply with modern Android storage rules.
What's Wrong with Your Current Code?
- On Android 10+, direct writes to
Environment.getExternalStorageDirectory()are blocked by scoped storage, even withrequestLegacyExternalStorage="true"(this is a temporary workaround that doesn't work reliably on Android 11+). - You've added unnecessary permissions (like
READ_INTERNAL_STORAGE,MANAGE_INTERNAL_STORAGE) that don't help and can confuse permission handling. MediaScannerConnection.scanFilemight not trigger correctly on newer versions when using direct file paths, since the system can't track the file properly.
Step-by-Step Fix
1. Clean Up Your AndroidManifest Permissions
Keep only the necessary permissions, and limit WRITE_EXTERNAL_STORAGE to Android 9 and below (since it's irrelevant for scoped storage):
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- Only needed for Android 9 (API 28) and lower --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <!-- Remove all internal storage and MANAGE_EXTERNAL_STORAGE permissions unless you have a specific, approved use case --> <application android:allowBackup="true" android:hardwareAccelerated="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:largeHeap="true" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme"> <!-- Keep your activities and FileProvider as is --> </application>
2. Replace Your Save Logic with MediaStore
Instead of writing directly to a file, use MediaStore to insert the image into the gallery. This is the official, recommended way to save media to shared storage on Android 10+:
public void SaveClick(View view) { // Handle permission check first (added in next step) ConstraintLayout savingLayout = findViewById(R.id.layoutavatar); Bitmap bitmap = getBitmapFromView(savingLayout); if (saveImageToGallery(this, bitmap)) { showSuccessDialog(); } else { Toast.makeText(this, "Failed to save avatar", Toast.LENGTH_SHORT).show(); } } private boolean saveImageToGallery(Context context, Bitmap bitmap) { String displayName = "AMONGUS-AVATAR_" + System.currentTimeMillis() + ".jpg"; String mimeType = "image/jpeg"; ContentValues values = new ContentValues(); values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName); values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType); // For Android 10+, set relative path to Pictures directory if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/AMONGUS-AVATAR"); values.put(MediaStore.MediaColumns.IS_PENDING, 1); } else { // For Android 9 and below, use external storage path (requires WRITE_EXTERNAL_STORAGE permission) File picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); File avatarDir = new File(picturesDir, "AMONGUS-AVATAR"); if (!avatarDir.exists() && !avatarDir.mkdirs()) { return false; } File imageFile = new File(avatarDir, displayName); values.put(MediaStore.MediaColumns.DATA, imageFile.getAbsolutePath()); } ContentResolver resolver = context.getContentResolver(); Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); if (uri == null) { return false; } try (OutputStream outputStream = resolver.openOutputStream(uri)) { return bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); } catch (IOException e) { e.printStackTrace(); // Delete the invalid entry if save fails resolver.delete(uri, null, null); return false; } finally { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { values.clear(); values.put(MediaStore.MediaColumns.IS_PENDING, 0); resolver.update(uri, values, null, null); } } } // Keep your getBitmapFromView method as it is private Bitmap getBitmapFromView(View view) { Bitmap returnedBitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(returnedBitmap); Drawable bgDrawable = view.getBackground(); if (bgDrawable != null) { bgDrawable.draw(canvas); } else { canvas.drawColor(Color.TRANSPARENT); } view.draw(canvas); return returnedBitmap; } private void showSuccessDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(fut.this); builder.setCancelable(true); builder.setTitle("AVATAR HAS BEEN SAVED"); builder.setMessage("YOUR AVATAR HAS BEEN SAVED SUCCESSFULLY TO YOUR IMAGES DIRECTORY"); builder.setPositiveButton("OK", (dialogInterface, i) -> alertTextView.setVisibility(View.VISIBLE)); builder.show(); }
3. Add Dynamic Permission Handling (For Android 6-9)
On Android 6.0 (API 23) to Android 9 (API 28), you need to request WRITE_EXTERNAL_STORAGE permission at runtime before saving the image. Update your SaveClick method and add the permission result handler:
public void SaveClick(View view) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100); return; } } // Proceed with save logic ConstraintLayout savingLayout = findViewById(R.id.layoutavatar); Bitmap bitmap = getBitmapFromView(savingLayout); if (saveImageToGallery(this, bitmap)) { showSuccessDialog(); } else { Toast.makeText(this, "Failed to save avatar", Toast.LENGTH_SHORT).show(); } } // Handle permission request result @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == 100) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Permission granted, retry save SaveClick(findViewById(R.id.layoutavatar)); } else { Toast.makeText(this, "Permission required to save avatar", Toast.LENGTH_SHORT).show(); } } }
Why This Works
- MediaStore is the official API for sharing media with the system gallery, which automatically handles scoped storage rules on Android 10+.
- We're using
Environment.DIRECTORY_PICTURESto save the image in the standard Pictures directory, which is visible to the gallery app. - The
IS_PENDINGflag on Android 10+ ensures the image isn't visible to the gallery until we finish writing it.
内容的提问来源于stack exchange,提问作者Ayoub E Cher

