Last active
March 29, 2024 11:52
-
-
Save shingming/a1045eb8a6b299e231fa5fd027636ec8 to your computer and use it in GitHub Desktop.
Unity3D: Save Image to Android Gallery (Test on API level 28 - 33)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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