imageSaver.saveBase64Image permission error on Android but it works on iOS, trying to save image to gallery

What platform are you building for?

Android

What framework are you building with?

Cordova

What is your build ID?

6063392c-9f84-4e0b-be10-547d9f613e05

What is your error message?

Permission denied

Please describe the issue:

Hello, I’m trying to save an image to photo’s gallery using window.imageSaver.saveBase64Image method. It works perfectly on iOS, but on Android I get a “Permissions denied” error. I’ve tried several solutions, but no sucess. If anyone can help, I’d appreciate it! Here’s the function and config.xml.

function saveImage(desenhoDoCanvas) {
var params = {data: desenhoDoCanvas, mediaScanner: true};
myApp.showIndicator();
window.imageSaver.saveBase64Image(params,
function (filePath) {
myApp.hideIndicator();
//myApp.alert(“Image saved!”, “”);
},
function (msg) {
myApp.hideIndicator();
myApp.alert("Image save error! Error: " + msg, “”);
}
);
};

<?xml version='1.0' encoding='utf-8'?>
<widget version="1.3.7" id="br.com.hrza" xmlns="http://www.w3.org/ns/widgets" xmlns:gap="http://phonegap.com/ns/1.0">
  <name>Instituto</name>
  <description>
        Instituto - Educação e Consultoria
  </description>
  <author email="ates@hia.com.br" href="http://ha.com.br">
        Integração e Tecnologia
  </author>
  <content src="index.html" />
	
  <access origin="*" />
  <allow-navigation href="*" />
	<!-- //?? <access origin="cdvfile://" /> -->
	<allow-navigation href="file:*" />
	<allow-navigation href="data:*" />
  <allow-intent href="*" />
  <allow-intent href="http://*/*" />
  <allow-intent href="https://*/*" />
  <allow-intent href="tel:*" />
  <allow-intent href="sms:*" />
  <allow-intent href="mailto:*" />
  <allow-intent href="geo:*" />
	
	<preference name="SplashScreenDelay" value="2000" />
	<preference name="AutoHideSplashScreen" value="true" />	
  <preference name="AndroidInsecureFileModeEnabled" value="true" />
	<preference name="AndroidWindowSplashScreenAnimatedIcon" value="resources/splashTemplate.png" />
	
  <preference name="AllowNewWindows" value="true" />
	<preference name="AndroidPersistentFileLocation" value="Compatibility" />
	<preference name="AndroidExtraFilesystems" value="files,files-external,documents,sdcard,root" />
	

  <!-- //?? 
	<platform name="android">
    <config-file target="AndroidManifest.xml" parent="/*">
      <uses-permission android:name="android.permission.CAMERA" />
      <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
      <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    </config-file>
		
		<edit-config file="AndroidManifest.xml" target="/*/uses-permission" mode="merge">
        <uses-permission android:name="android.permission.CAMERA" />
        <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    </edit-config>
    <edit-config file="AndroidManifest.xml" target="/*" mode="merge">
        <uses-feature android:name="android.hardware.location.gps" android:required="false" />
        <uses-feature android:name="android.hardware.camera" android:required="false" />
        <uses-feature android:name="android.hardware.camera.autofocus" />
    </edit-config>		
  </platform>
	-->
	
  <platform name="android">
		<preference name="AndroidWindowSplashScreenBackground" value="#000000" />        
		<feature name="http://api.phonegap.com/1.0/file" />
    <feature name="http://api.phonegap.com/1.0/file" />
    <feature name="File">
        <param name="android-package" value="org.apache.cordova.file.FileUtils" />
    </feature>
    <feature name="FileTransfer">
        <param name="android-package" value="org.apache.cordova.filetransfer.FileTransfer" />
    </feature>
    <feature name="Storage">
        <param name="android-package" value="org.apache.cordova.Storage" />
    </feature>
    <feature name="http://api.phonegap.com/1.0/network" />
    <allow-intent href="market:*" />
		<!--
		<config-file target="AndroidManifest.xml" parent="/*" xmlns:android="http://schemas.android.com/apk/res/android">
      <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
		</config-file>		
		-->
		
  </platform>

  <platform name="ios">
		<preference name="permissions" value="none" />		
		<preference name="WKWebViewOnly" value="true" />
		<feature name="CDVWKWebViewEngine">
		<param name="ios-package" value="CDVWKWebViewEngine" />
		</feature>
		<preference name="CordovaWebViewEngine" value="CDVWKWebViewEngine" />
        <config-file parent="NSCameraUsageDescription" target="*-Info.plist">
            <string>Este aplicativo precisa do acesso a camera para ler QRcode e acessar a sua unidade!</string>
        </config-file>
        <config-file parent="NSLocationAlwaysAndWhenInUseUsageDescription" target="*-Info.plist">
            <string>Este aplicativo usa sua localização para saber se você está próximo da sua unidade!</string>
        </config-file>
        <config-file parent="NSLocationWhenInUseUsageDescription" target="*-Info.plist">
            <string>Este aplicativo usa sua localização para saber se você está próximo da sua unidade!</string>
        </config-file>
				<edit-config target="NSPhotoLibraryUsageDescription" file="*-Info.plist" mode="merge">
						<string>Este aplicativo usa a galeria de fotos para envio de documentação para sua unidade!</string>
				</edit-config>
				<edit-config target="NSPhotoLibraryAddUsageDescription" file="*-Info.plist" mode="merge">
						<string>Este aplicativo usa a galeria de fotos para salvar seu certificado!</string>
				</edit-config>
        <feature name="File">
            <param name="ios-package" value="CDVFile" />
        </feature>
        <feature name="FileTransfer">
            <param name="ios-package" value="CDVFileTransfer" />
        </feature>
        <allow-intent href="itms:*" />
        <allow-intent href="itms-apps:*" />
		<edit-config file="*-Info.plist" mode="merge" target="CFBundleDisplayName">
        <string>Instituto Avanza</string>
    </edit-config>
  </platform>

  <preference name="DisallowOverscroll" value="true" />
	<preference name="loadUrlTimeoutValue" value="100000" />
	<preference name="AndroidXEnabled" value="true" />

	<plugin name="cordova-plugin-statusbar"/>
  <preference name="StatusBarOverlaysWebView" value="false" />
  <preference name="StatusBarBackgroundColor" value="#2c7376" />
  <preference name="StatusBarStyle" value="lightcontent" />
  <plugin name="cordova-plugin-device"/>
  <plugin name="cordova-plugin-device-motion"/>
  <plugin name="cordova-plugin-device-orientation"/>
  <plugin name="cordova-plugin-inappbrowser"/>
  <plugin name="cordova-plugin-network-information"/>
  <plugin name="cordova-plugin-vibration"/>
  <plugin name="cordova-plugin-wkwebview-engine"/>
	<plugin name="@ahovakimyan/cordova-plugin-wkwebviewxhrfix"/>
	<plugin name="cordova-plugin-android-permissions" />
  <plugin name="cordova-plugin-cleartext" />

	<plugin name="cordova-plugin-wkwebview-file-xhr" version="~3.1.0"/>
  <plugin name="cordova-plugin-splashscreen" version="~6.0.2"/>
	<!-- IOS só funcionou quando coloquei aqui.... tirar para compilar para Android e colocar a versão 3.3.0 -->
	<!--<plugin name="onesignal-cordova-plugin" spec="^2.4.1" version="~2.4.1" source="npm"/> -->
	<!--<plugin name="onesignal-cordova-plugin" spec="^3.3.0" version="~3.3.0" source="npm"/>-->
  <plugin name="phonegap-plugin-barcodescanner-android12"/>
	<plugin name="cordova-plugin-save-image-gallery"/>
	<!-- //?? <plugin name="cordova-plugin-file"/>-->
  <plugin name="cordova-plugin-camera"/>
	<plugin name="cordova-plugin-file@8.0.0"/>
	
	<plugin name="cordova-plugin-file-transfer"/>
	<plugin name="cordova-plugin-gallery-refresh"/>
  <!--<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />	-->
</widget>

Have you tried remote debugging to see if more details appear in the console?

It’s possible that you need more permissions on Android. File permissions have changed vastly over the last 7 years and I suspect that you may need to request further access. See if the console gives you further information.

No, I haven’t tried remote debugging yet, but it’s definitely a permissions issue. I tried it on Android 9 and it worked fine, but it didn’t work on Android 13.

Remote debugging by Browserstack (app does not show any message asking for permission, not even it is possible in the phone configuration manually):

07-24 12:38:30.041 D/SaveImageGallery(23639): Requesting permissions for WRITE_EXTERNAL_STORAGE
07-24 12:38:30.249 D/SaveImageGallery(23639): Permission not granted by the user
07-24 12:38:30.249 D/CompatibilityChangeReporter(23639): Compat change id reported: 78294732; UID 10516; state: ENABLED
07-24 12:38:30.249 D/SaveImageGallery(23639): Permission not granted by the user

I notice you have some permissions for Android duplicated in your config.xml. I would suggest merging those into a single config-file block. You also have two Android platform blocks which I would merge.

Despite this, I doubt this will work, as permissions changed vastly for Android 11. If you target SDK 28, it’s possible the plugin will work, but in all likelihood the plugin needs updates.

Let me know if I can help further.

Hi… it worked with code:

function salvarImagemAndroid(base64String) {
	// String base64 de teste (substitua pela sua)
	//const base64String = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z/C/HgAG/wJ/p35L8wAAAABJRU5ErkJggg==";
	const fileName = "minha_imagem_" + Date.now() + ".png";
	const base64Data = base64String.replace(/^data:image\/(png|jpg|jpeg);base64,/, "");

	// Tenta salvar em /Pictures
	window.resolveLocalFileSystemURL(
			cordova.file.externalRootDirectory + 'Pictures/',
			function(dirEntry) {
					//alert('Diretório Pictures acessado: ' + dirEntry.fullPath);
					dirEntry.getFile(fileName, { create: true, exclusive: false }, 
							function(fileEntry) {
									//alert('Arquivo criado: ' + fileEntry.name);
									fileEntry.createWriter(function(fileWriter) {
											fileWriter.onwriteend = function() {
													const message = 'Imagem salva com sucesso em: ' + fileEntry.nativeURL;
													//document.getElementById('status').innerText = message;
													//alert(message);
													alert(message);
													// Tentar registrar no MediaStore (Android 13+)
													registerInMediaStore(fileEntry);
											};
											fileWriter.onerror = function(e) {
													const errorMessage = 'Erro ao salvar a imagem: ' + e.toString();
													//document.getElementById('status').innerText = errorMessage;
													console.error(errorMessage);
													alert(errorMessage);
											};

											// Converte base64 para Blob
											try {
													const byteCharacters = atob(base64Data);
													const byteNumbers = new Array(byteCharacters.length);
													for (let i = 0; i < byteCharacters.length; i++) {
															byteNumbers[i] = byteCharacters.charCodeAt(i);
													}
													const byteArray = new Uint8Array(byteNumbers);
													const blob = new Blob([byteArray], { type: 'image/png' });
													fileWriter.write(blob);
											} catch (e) {
													const errorMessage = 'Erro ao converter base64: ' + e.toString();
													//document.getElementById('status').innerText = errorMessage;
													console.error(errorMessage);
													alert(errorMessage);
											}
									}, fail);
							}, fail);
			}, fail);
};

function fail(error) {
	const errorMessage = 'Erro: ' + error.code + ' - ' + error.message;
	//document.getElementById('status').innerText = errorMessage;
	console.error(errorMessage);
	alert(errorMessage);
};