Skip to content

Instantly share code, notes, and snippets.

@shingming
Last active March 29, 2024 11:52
Show Gist options
  • Save shingming/a1045eb8a6b299e231fa5fd027636ec8 to your computer and use it in GitHub Desktop.
Save shingming/a1045eb8a6b299e231fa5fd027636ec8 to your computer and use it in GitHub Desktop.
Unity3D: Save Image to Android Gallery (Test on API level 28 - 33)
using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SaveImage2AndroidGallery
{
/// <summary>
/// Save the image byte data to the Android gallery.
/// The byte data should be encoded to the format that is noted in the enum "PictureOutputFormat."
/// Please note this function <b>needs to be granted the permission of "WRITE_EXTERNAL_STORAGE"
/// if Android API levels are less than 29.</b>
/// </summary>
/// <param name="imageByteData">The image data which is encoded</param>
/// <param name="imageNameWithFileExtension">The image name included its extension</param>
/// <param name="imageAlbumName">The album name that the photo will be saved to</param>
/// <returns>The result of image saving. True if saved successfully. Otherwise, return false.</returns>
public static bool SaveImageToAndroidGallery(
in byte[] imageByteData,
in string imageNameWithFileExtension,
in string imageAlbumName = "")
{
if (imageByteData == null || string.IsNullOrEmpty(imageNameWithFileExtension))
{
Debug.LogError(
"The input data is not valid. Please check the data and try again.");
return false;
}
const string mediaColumnsTitle = "title";
const string mediaColumnsDisplayName = "_display_name";
const string mediaColumnsMimeType = "mime_type";
const string mediaColumnsRelativePath = "relative_path";
const string mediaColumnsIsPending = "is_pending";
const string mediaColumnsDate = "_data";
const int androidQ = 29;
using var buildVersionClass = new AndroidJavaClass(ANDROID_CLASS_OS_BUILD_VERSION);
var buildVersionSdkInt = buildVersionClass.GetStatic<int>("SDK_INT");
AndroidJavaObject currentActivity = Activity;
int fileExtensionIndex = imageNameWithFileExtension.LastIndexOf(".", StringComparison.Ordinal);
if (fileExtensionIndex <= 0)
{
Debug.LogError(
"The file extension get failed. Please check the input parameter, \"imageNameWithFileExtension\", and try again.");
return false;
}
string imageNameWithoutFileExtension = imageNameWithFileExtension.Substring(0, fileExtensionIndex);
string fileExtension = imageNameWithFileExtension.Substring(fileExtensionIndex + 1).ToUpper();
var mimeType = string.Empty;
foreach (string item in Enum.GetNames(typeof(PictureOutputFormat)))
{
if (fileExtension != item)
{
continue;
}
mimeType = $"image/{item}";
break;
}
if (string.IsNullOrEmpty(mimeType))
{
Debug.LogError("The image output format is not supported.");
return false;
}
using var contentValues = new AndroidJavaObject(ANDROID_CLASS_CONTENT_VALUES);
contentValues.Call("put", mediaColumnsTitle, imageNameWithoutFileExtension);
contentValues.Call("put", mediaColumnsDisplayName, imageNameWithFileExtension);
contentValues.Call("put", mediaColumnsMimeType, mimeType);
using var contentResolver = currentActivity.Call<AndroidJavaObject>("getContentResolver");
if (contentResolver == null)
{
Debug.LogError("Cannot get the Android content resolver.");
return false;
}
using var mediaClass = new AndroidJavaClass(ANDROID_CLASS_MEDIA_STORE_IMAGE_MEDIA);
using var externalContentUri = mediaClass.GetStatic<AndroidJavaObject>("EXTERNAL_CONTENT_URI");
if (externalContentUri == null)
{
Debug.LogError("The Android uri of external content is not valid.");
return false;
}
using var androidEnvClass = new AndroidJavaClass(ANDROID_CLASS_OS_ENVIRONMENT);
var imageDirectory = androidEnvClass.GetStatic<string>("DIRECTORY_PICTURES");
if (string.IsNullOrEmpty(imageDirectory))
{
Debug.LogError("The directory for saving the image is not valid.");
return false;
}
string relativePath = string.IsNullOrEmpty(imageAlbumName)
? imageDirectory
: Path.Combine(imageDirectory, imageAlbumName);
if (buildVersionSdkInt >= androidQ)
{
// New method of Android system (API level >= 29)
Debug.Log("Save image to Android gallery by new method.");
contentValues.Call("put", mediaColumnsRelativePath, relativePath);
// An error will occur on some brand of Android system.
// To avoid an error, the part that adds "is_pending" is placed in the try-catch block.
try
{
contentValues.Call("put", mediaColumnsIsPending, 1);
}
catch (Exception e)
{
Debug.LogWarning($"Error on calling writing the data at media column \"is_pending\": {e}.");
}
using var insertImageUri =
contentResolver.Call<AndroidJavaObject>("insert", externalContentUri, contentValues);
if (insertImageUri == null)
{
Debug.LogError("Insert the image to external content uri failed.");
return false;
}
try
{
using var imageFileUri =
contentResolver.Call<AndroidJavaObject>("openOutputStream", insertImageUri);
imageFileUri.Call("write", imageByteData);
imageFileUri.Call("flush");
imageFileUri.Call("close");
}
catch (Exception e)
{
Debug.LogError($"The image file URI could not be opened: {e}");
return false;
}
// An error will occur on some brand of Android system.
// To avoid an error, the part that adds "is_pending" is placed in the try-catch block.
try
{
contentValues.Call("put", mediaColumnsIsPending, 0);
}
catch (Exception e)
{
Debug.LogError($"Error on calling writing the data at media column \"is_pending\": {e}.");
}
contentResolver.Call<int>("update", insertImageUri, contentValues, null, null);
}
else
{
// Old method of Android system (API level < 29)
Debug.Log("Save image to Android gallery by old method.");
try
{
using var imageDirectoryFile = androidEnvClass.CallStatic<AndroidJavaObject>(
"getExternalStoragePublicDirectory",
imageDirectory);
using var imageDirectoryWithAlbumFile = new AndroidJavaObject(
JAVA_CLASS_IO_FILE,
imageDirectoryFile, imageAlbumName);
imageDirectoryWithAlbumFile.Call<bool>("mkdirs");
using var imageFile = new AndroidJavaObject(
JAVA_CLASS_IO_FILE,
imageDirectoryWithAlbumFile, imageNameWithFileExtension);
var imageFileAbsolutePath = imageFile.Call<string>("getAbsolutePath");
using var fileSourceStream = new MemoryStream(imageByteData);
using FileStream fileOutputStream = System.IO.File.Create(imageFileAbsolutePath);
fileSourceStream.CopyTo(fileOutputStream);
fileOutputStream.Close();
fileSourceStream.Close();
contentValues.Call("put", mediaColumnsDate, imageFileAbsolutePath);
contentResolver.Call<AndroidJavaObject>("insert", externalContentUri, contentValues);
using var androidContentIntentClass =
new AndroidJavaClass(ANDROID_CLASS_CONTENT_INTENT);
using var mediaScanIntent = new AndroidJavaObject(
ANDROID_CLASS_CONTENT_INTENT,
androidContentIntentClass.GetStatic<string>("ACTION_MEDIA_SCANNER_SCAN_FILE"));
using var androidUriClass = new AndroidJavaClass(ANDROID_CLASS_NET_URI);
using var imageFileUri = androidUriClass.CallStatic<AndroidJavaObject>("fromFile", imageFile);
mediaScanIntent.Call<AndroidJavaObject>("setData", imageFileUri);
currentActivity.Call("sendBroadcast", mediaScanIntent);
}
catch (Exception e)
{
Debug.LogError("Error on saving the image to external storage: " + e);
return false;
}
}
return true;
}
private static AndroidJavaObject _activity;
public static AndroidJavaObject Activity
{
get
{
if (_activity != null)
{
return _activity;
}
var unityPlayer = new AndroidJavaClass(ANDROID_CLASS_UNITY_PLAYER);
_activity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
return _activity;
}
}
public const string ANDROID_CLASS_UNITY_PLAYER = "com.unity3d.player.UnityPlayer";
public const string ANDROID_CLASS_MEDIA_STORE_IMAGE_MEDIA = "android.provider.MediaStore$Images$Media";
public const string ANDROID_CLASS_GRAPHICS_BITMAP_FACTORY = "android.graphics.BitmapFactory";
public const string ANDROID_CLASS_GRAPHICS_BITMAP_COMPRESS_FORMAT = "android.graphics.Bitmap$CompressFormat";
public const string ANDROID_CLASS_OS_ENVIRONMENT = "android.os.Environment";
public const string ANDROID_CLASS_OS_BUILD_VERSION = "android.os.Build$VERSION";
public const string ANDROID_CLASS_CONTENT_INTENT = "android.content.Intent";
public const string ANDROID_CLASS_CONTENT_VALUES = "android.content.ContentValues";
public const string ANDROID_CLASS_NET_URI = "android.net.Uri";
public const string JAVA_CLASS_IO_FILE = "java.io.File";
public const string JAVA_CLASS_IO_OUTPUTSTREAM = "java.io.OutputStream";
public const string JAVA_CLASS_IO_BYTEARRAYOUTPUTSTREAM = "java.io.ByteArrayOutputStream";
public const string JAVA_CLASS_IO_BYTEARRAYINPUTSTREAM = "java.io.ByteArrayInputStream";
public enum PictureOutputFormat
{
PNG,
JPG
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment