Compressing base64 encoded images in React-Native on Android does not recognise 'data' protocol - react-native

Issue
Within a React-Native (0.43) application we are using a component that uses a SectionList to render photos sorted by day. Each section can contain multiple images. Photos are taken using the react-native-image-crop-picker library and uploaded to the backend, or queued locally if no internet connection is available, encoded in base64 format. The image resolution is set to 800x800 pixels (requirement for other purposes of the images). On phones with lower memory, rendering ~20 images will crash the app due to insufficient memory. This issue can only be reproduced on low-end Android phones but I expect this to be a low memory issue and not related to the OS. To tackle this issue, thumbnails need to be generated to test if this is the case. Independent of when these thumbnails are generated (before sending to server or on-the-fly before loading component). The code below works fine for iOS but for Android it throws the error: Unknown protocol: data which comes from the ImageEditor.cropImage() function.
Snippet from main .js file
//The mergeReduxWithMeteor function takes care of merging the redux state,
//containing images not yet uploaded to the server,
//and the Meteor data as received by the server.
//No issues here...
helpers.mergeReduxWithMeteor(props.photoStore, props.SynergySummaryReady ? props.SynergyData : [])
//The imageHelper.compressPhoto does the actual compression
//No issues with the promise chain as is.
.then((data) => {
return Promise.all(data.map(imageHelper.compressPhoto))
})
// The remaining functions take care of the formatting for the SectionList.
// No issues here either... :)
.then((data) => {
return helpers.clusterDataByDay(data)
})
//We populate the resulting data in the state that is used for the SectionList
.then((data) => {
this.setState({NotHorusData: data})
})
.catch((error) => console.error(error))
imageHelper.compressphoto()
export function compressPhoto(photo) {
return new Promise((resolve, reject) => {
let imageSize = {
offset: {
x: 0,
y: 0
},
size: {
width: IMAGE_SIZE,
height: IMAGE_SIZE
},
displaySize: {
width: IMAGE_TARGET_SIZE,
height: IMAGE_TARGET_SIZE,
},
resizeMode: 'contain'
}
ImageEditor.cropImage(`data:image/jpeg;base64,${photo.data.userPhoto}`, imageSize, (imageURI) => {
ImageStore.getBase64ForTag(imageURI, (base64Data) => {
resolve({
...photo,
data: {
...photo.data,
userPhoto: base64Data,
}
})
}, (error) => reject(error))
}, (error) => reject(error))
})
}
Approach 1: Fix data protocol issue on Android
Issue on Github from RN addresses the same issue but no solution is provided.
Approach 2: Bypass data protocol issue by providing uri on Android
Although less favourable due to the added communication/delay, another approach is to avoid the data protocol issue by providing a temporary URI provided by ImageStore. See the adapted code below for Android.
if(Platform.OS === 'android'){
ImageStore.addImageFromBase64(`data:image/jpeg;base64,${photo.data.userPhoto}`, (tempURI) => {
ImageEditor.cropImage(tempURI, imageSize, (imageURI) => {
ImageStore.getBase64ForTag(imageURI, (base64Data) => {
ImageStore.removeImageForTag(tempURI)
resolve({
...photo,
data: {
...photo.data,
userPhoto: base64Data,
}
})
}, (error) => reject(error))
}, (error) => reject(error))
}, (error) => reject(error))
}
Unfortunately ImageStore.addImageFromBase64 is not recognised on Android.
Does anyone have any experience with ImageEditor and ImageStore on Android that might be helpful in this situation? Any other approaches are welcome too!

I managed to resolve the issue with the use of react-native-fetch-blob and react-native-image-resizer for both iOS and Android. Performance is unexpectedly good in comparison to the implementation in the question above. I shared the code below for others to use :)
export function compressPhoto(photo) {
return new Promise((resolve, reject) => {
let tempUri = `${cache}/Vire_${photo.data.userPhotoDate}.jpg`
fs.writeFile(tempUri, photo.data.userPhoto, "base64")
.then(() => {
ImageResizer.createResizedImage(
`file:${tempUri}`, IMAGE_TARGET_SIZE, IMAGE_TARGET_SIZE, "JPEG", 100, 0).then((resizedImageUri) => {
fs.readFile(`${resizedImageUri}`, "base64")
.then( data => {
resolve({...photo, data: { ...photo.data, userPhoto: data }})
})
.catch(error => reject(`readFile:error: ${error}`))
},
(error) => reject(`createResizedImage:error: ${error}`)
)
})
.catch( error => {
reject(`writeFile:error: ${error}`)
})
})
}
The gist is storing base64 encoded picture in the cache-directory and using imageResizer to fetch the image, compress it, and read it again in base64 for use.

Related

Expo Document picker does not give back the correct uri

So I am trying to use the expo document picker (https://docs.expo.io/versions/latest/sdk/document-picker/) to let the user select an image from their phones file system and display it on the screen. It was working quite recently until I noticed that the promise it sends back stopped returning the correct uri. Before it was returning a uri starting with file:///data/ where as now it returns a uri starting with /data. This new type of uri does not display on the screen and I am wondering how I get back to the old one? I am using await DocumentPicker.getDocumentAsync to get the file in the first place.
I had the exact same problem after upgrading from expo 38 to 42. I did a workaround using expo-file-system.
First I set the copyToCacheDirectory option to false
const result = await DocumentPicker.getDocumentAsync({
type:'*/*',
copyToCacheDirectory: false,
});
That would save the file to the conten:// file space. Once we have the file we can copy it to our file:// location:
const uri = FileSystem.documentDirectory+result.name;
await FileSystem.copyAsync({
from: result.uri,
to: uri
})
Now you should be able to use uri (which includes the file:// path) without problems.
I have same problem from expo SDK 38 to SDK 42,
I find a workaround from here
const encode = uri => {
if (Platform.OS === 'android') return encodeURI(`file://${uri}`)
else return uri
}
...
<Button
onPress = { async () => {
let result = await DocumentPicker.getDocumentAsync({ type: '*/*' })
if (result.type === "success") {
console.log("getDocumentAsync", result);
Alert.alert(
"upload file",
`Whether to upload ${result.name}`,
[
{
text: "Cancel",
onPress: () => {},
style: "cancel"
},
{
text: "OK",
onPress: () => dispatch(action.sendFile(encode(result.uri)))
}
],
{ cancelable: true }
)
}
}}
/>
it work for me!
I you want the file to be accessible for using in WebView, You can do this
const result = await DocumentPicker.getDocumentAsync({
type: "application/pdf",
copyToCacheDirectory: false,
});
This is will give you a uri like this
{
...,
"uri": "content://com.android.externalstorage.documents/document/home%3Ayour-file.pdf",
}
and you can access the file by passing it as uri
{{
uri: uri,
}}

Uncaught (in promise) : NotSupportedError: GATT Error Unknown

I am trying to write data which is 490 in length to a device using Web Bluetooth API. When I try to write I get NotSupportedError: GATT Error Unknown.
I am using chrome browser on android.
My app is in Angular 7 and I am using #types/web-bluetooth.
Below is the code:
navigator.bluetooth
.requestDevice({
filters: [{ name: this.deviceName }],
optionalServices: [this.GATT_SERVICE],
})
.then((device) => {
return device.gatt.connect();
})
.then((server) => {
return server.getPrimaryService(this.GATT_SERVICE);
})
.then((service) => {
this.gattService = service;
return this.gattService.getCharacteristic(this.GATT_CHAR);
})
.then((characteristic) => {
characteristic.writeValueWithResponse(this.arraybufferdata);
})
.catch(async (err) => {
console.log(err);
});
Can someone please help?
Is the issue related to the length? Can you reproduce error with less bytes in this.arraybufferdata?
Nit: You may want to return promise so that error gets propagated.
.then((characteristic) => {
return characteristic.writeValueWithResponse(this.arraybufferdata);
})

Detect All Beacons in the region and get its data ( REACT NATIVE )

Hope you are doing well.
i want to detect the beacons which are in my range. i got the react-native-beacons-manager but its not working.
i have the beacon device. i tested it but not luck.
As per this code its showing only those UUID which i am passing in the region object.
Can anyone help me out how to detect the nearby beacons. the code i got from the library is :
the source link is
https://github.com/MacKentoch/react-native-beacons-manager
i just did nothing just console the data using in example in above link.
my code is
componentWillMount() {
Beacons.requestAlwaysAuthorization();
const region = {
identifier: 'pBeacon_n',
uuid: '7BA5D5CE-C416-5FD6-8AAA-919D534E0DC3'
};
Beacons.startMonitoringForRegion(region) // or like < v1.0.7: .startRangingBeaconsInRegion(identifier, uuid)
.then(() => console.warn('Beacons monitoring started succesfully'))
.catch(error => console.warn(`Beacons monitoring not started, error: ${error}`));
// Range for beacons inside the region
Beacons.startRangingBeaconsInRegion(region) // or like < v1.0.7: .startRangingBeaconsInRegion(identifier, uuid)
.then(() => console.warn('Beacons ranging started succesfully'))
.catch(error => console.warn(`Beacons ranging not started, error: ${error}`));
// update location to ba able to monitor:
Beacons.startUpdatingLocation();
}
componentDidMount() {
this.beaconsDidRangeEvent = DeviceEventEmitter.addListener(
'beaconsDidRange',
(data) => {
console.warn('beaconsDidRange data: ', data);
}
);
}
The UUID i am passing to the region is my real Beacon UUID.
Thanks.
Unfortunately iOS simply does not let you search for iBeacon transmissions without specifying the ProximityUUID. The lowest level native APIs which this ReactNative Module uses have this restriction. Apple implements it this way as a security restriction to prevent you from scanning for beacons belonging to others.
use like this you can detect all beacons with out giving uuid and etc, but its not working in ios.
startBeacon = () => {
Reactotron.log('else');
Beacons.detectIBeacons();
Beacons.detectAltBeacons();
Beacons.detectEstimotes();
Beacons.detectEddystoneUID();
Beacons.detectEddystoneTLM();
Beacons.detectEddystoneURL();
Beacons.startRangingBeaconsInRegion('REGION1')
.then((data) => {
Alert.alert('', `Beacons monitoring started!`);
})
.catch((error) => {
Alert.alert('', `Beacons start error!`);
});
DeviceEventEmitter.addListener('beaconsDidRange', (data) => {
if (data.beacons.length) {
this.setState({ detectedBeacons: data.beacons });
Alert.alert(`Found beacons!`, JSON.stringify(data.beacons));
}
});
}

Upload images from React Native to LoopBack

I need to upload a selection of images that user picked from CameraRoll to the LoopBack Component Storage. The thing is that the component storage is working fine, because I can upload and download the files through Postman. But, when I try to upload from react native to loopback, it always returns "No file content upload" with http status 400.
I read a lot of people talking about it and tried everything and none worked for me.
First, I am taking the images from the CameraRoll and my images array looks like this:
[
{
exists: 1,
file: "assets-library://asset/asset.JPG?id=3FF3C864-3A1A-4E55-9455-B56896DDBF1F&ext=JPG",
isDirectory: 0,
md5: "428c2e462a606131428ed4b45c695030",
modificationTime: 1535592967.3309255,
size: 153652,
uri: null
}
]
In the example above I just selected one image.
I transformed to Blob, then I got:
[
{
_data: {
blobId: "3FF3C864-3A1A-4E55-9455-B56896DDBF1F",
name: "asset.JPG",
offset: 0,
size: 153652,
type: "image/jpeg"
}
}
]
So I tried a lot of things after this, tried to send the blob itself as the request body, tried to append to a form data and send the form data, but it doesn't matter the way I try, I always get the "No file content upload" response.
I also tried the example from Facebook, didn't work: https://github.com/facebook/react-native/blob/master/Libraries/Network/FormData.js#L28
The way I am trying now:
In my view:
finalizarCadastro = async () => {
let formData = new FormData();
let blobs = [];
for(let i=0;i<this.state.fotos.length;i++){
let response = await fetch(this.state.fotos[i]);
let blob = await response.blob();
blobs.push(blob);
}
formData.append("files", blobs);
this.props.servico.criar(formData);
}
And the function that send to my server:
criar: (servico) => {
this.setState({carregando: true});
axios.post(`${REQUEST_URL}/arquivos/seila/upload`, servico, {headers: {'content-type': 'multipart/form-data'}}).then(() => {
this.setState({carregando: false});
this.props.alertWithType("success", "Sucesso", "Arquivo salvo com sucesso");
}).catch(error => {
this.setState({carregando: false});
console.log(error.response);
this.props.alertWithType("error", "Erro", error.response.data.error.message);
})
}
I found the solution. So the problem was actually not the code itself, the problem was sending multiple files at the same time. To fix everything, I did this:
this.state.fotos.forEach((foto, i) => {
formData.append(`foto${i}`, {
uri: foto,
type: "image/jpg",
name: "foto.jpg"
});
})
this.props.servico.criar(formData);
And my function that sends the request to the server:
criar: (servico) => {
this.setState({carregando: true});
axios.post(`${REQUEST_URL}/arquivos/seila/upload`, servico).then((response) => {
this.setState({carregando: false});
this.props.alertWithType("success", "Sucesso", "Arquivo salvo com sucesso");
}).catch(error => {
this.setState({carregando: false});
this.props.alertWithType("error", "Erro", error.response.data.error.message);
})
},
So you don't need to set the Content-Type header to multipart/form-data and don't need to transform the images to blob, actually you just need the uri of each one, and I think the type and name attributes are opcional.

React Native - Timeout on fetching

How can I set a timeout when fetching an API?
What I want exactly is to try to fetch the data for 10 seconds, if it fails then I want to load the data (saved previously and updated every time that the fetch works) from AsyncStorage.
Probably how I'm doing this is not the correct way, I'm kinda noob at programming (xD). However, this code works just fine on the emulator but does not work on my phone (android). The AsyncStorage seems not to work.
Here is my code:
constructor(){
super()
this.state = {
fetching: false,
data: []
}
}
componentWillMount(){
this.setState({ fetching: true })
fetch('http://192.168.1.122:3000/categories.json')
.then(res => res.json())
.then(res => {
this.setState({
data: res,
fetching: false
})
})
.then(res => {
AsyncStorage.setItem(
'#Data:Monumentos',
JSON.stringify(res)
)
})
.catch(AsyncStorage.getItem(
'#Data:Monuments',
(err, dados) => {
if(err) {
console.error('Error loading monuments', err)
} else {
const monuments = JSON.parse(dados)
this.setState({
data: monuments
})
}
}
))
}
Hope you can help me. Thank you!
RN uses whatwg-fetch which doesn't have a timeout. You can work around it by using whatwg-fetch-timeout as mentioned here
That would be simpler than what Micheal above in the comments linked too which is to use Promise.race and setTimeout. Admittedly, pretty clever.