Fix app stuck and memory issues while uploading images (#2287)

* Do not use an image array to store all bitmap pixels at once

* Extract image preprocessing to a different service and use computation thread

* Add java docs

* Cleanup code to remove temp file logic

* Add logs in upload flow

* Fix tests

* Fix more tests
This commit is contained in:
Vivek Maskara 2019-01-16 22:02:25 +05:30 committed by neslihanturan
parent 21f82dd346
commit 559127dfa3
21 changed files with 320 additions and 891 deletions

View file

@ -1,155 +0,0 @@
package fr.free.nrw.commons.utils;
import android.content.Context;
import android.net.Uri;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Random;
import timber.log.Timber;
/**
* This class includes utility methods for uploading process of images.
*/
public class ContributionUtils {
private static String TEMP_EXTERNAL_DIRECTORY =
android.os.Environment.getExternalStorageDirectory().getPath()+
File.separatorChar+"UploadingByCommonsApp";
/**
* Saves images temporarily to a fixed folder and use Uri of that file during upload process.
* Otherwise, temporary Uri provided by content provider sometimes points to a null space and
* consequently upload fails. See: issue #1400A and E.
* Not: Saved image will be deleted, our directory will be empty after upload process.
* @return URI of saved image
*/
public static Uri saveFileBeingUploadedTemporarily(Context context, Uri URIfromContentProvider) {
// TODO add exceptions for Google Drive URİ is needed
Uri result = null;
if (checkIfDirectoryExists(TEMP_EXTERNAL_DIRECTORY)) {
String destinationFilename = decideTempDestinationFileName();
result = saveFileFromURI(context, URIfromContentProvider, destinationFilename);
} else { // If directory doesn't exist, create it and recursive call current method to check again
File file = new File(TEMP_EXTERNAL_DIRECTORY);
if (file.mkdirs()) {
Timber.d("saveFileBeingUploadedTemporarily() parameters: URI from Content Provider %s", URIfromContentProvider);
result = saveFileBeingUploadedTemporarily(context, URIfromContentProvider); // If directory is created
} else { //An error occurred to create directory
Timber.e("saveFileBeingUploadedTemporarily() parameters: URI from Content Provider %s", URIfromContentProvider);
}
}
return result;
}
/**
* Removes temp file created during upload
* @param tempFileUri
*/
public static void removeTemporaryFile(Uri tempFileUri) {
//TODO: do I have to notify file system about deletion?
File tempFile = new File(tempFileUri.getPath());
if (tempFile.exists()) {
boolean isDeleted = tempFile.delete();
Timber.e("removeTemporaryFile() parameters: URI tempFileUri %s, deleted status %b", tempFileUri, isDeleted);
}
}
/**
* Creates a temporary directory and returns pathname
* @return
*/
private static String decideTempDestinationFileName() {
int i = 0;
while (new File(TEMP_EXTERNAL_DIRECTORY + File.separatorChar + i + "_tmp").exists()) {
i++;
}
// Use time stamp for file name, so that two temporary file never has same file name
// to prevent previous file reference bug
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
// For multiple uploads, time randomisation should be combined with another random
// parameter, since they created at same time
int multipleUploadRandomParameter = new Random().nextInt(100);
return TEMP_EXTERNAL_DIRECTORY + File.separatorChar + timeStamp + multipleUploadRandomParameter + "_tmp";
}
/**
* Empties files in Temporary Directory
*/
public static void emptyTemporaryDirectory() {
File dir = new File(TEMP_EXTERNAL_DIRECTORY);
if (dir.isDirectory()) {
String[] children = dir.list();
if (children == null) return;
for (String child : children) {
new File(dir, child).delete();
}
}
}
/**
* Saves file from source URI to destination.
* @param sourceUri Uri which points to file to be saved
* @param destinationFilename where file will be located at
* @return Uri points to file saved
*/
private static Uri saveFileFromURI(Context context, Uri sourceUri, String destinationFilename) {
File file = new File(destinationFilename);
if (file.exists()) {
file.delete();
}
InputStream in = null;
OutputStream out = null;
try {
in = context.getContentResolver().openInputStream(sourceUri);
out = new FileOutputStream(new File(destinationFilename));
byte[] buf = new byte[1024];
int length;
while ((length = in.read(buf)) > 0) {
out.write(buf, 0, length);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (out != null) out.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (in != null) in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return Uri.parse("file://" + destinationFilename);
}
/**
* Checks if directory exists
* @param pathToCheck path of directory to check
* @return true if directory exists, false otherwise
*/
private static boolean checkIfDirectoryExists(String pathToCheck) {
File dir = new File(pathToCheck);
return dir.exists() && dir.isDirectory();
}
}

View file

@ -128,44 +128,42 @@ public class ImageUtils {
int bitmapHeight = bitmap.getHeight();
int allPixelsCount = bitmapWidth * bitmapHeight;
int[] bitmapPixels = new int[allPixelsCount];
Timber.d("total %s", Integer.toString(allPixelsCount));
bitmap.getPixels(bitmapPixels,0,bitmapWidth,0,0,bitmapWidth,bitmapHeight);
int numberOfBrightPixels = 0;
int numberOfMediumBrightnessPixels = 0;
double brightPixelThreshold = 0.025*allPixelsCount;
double mediumBrightPixelThreshold = 0.3*allPixelsCount;
double brightPixelThreshold = 0.025 * allPixelsCount;
double mediumBrightPixelThreshold = 0.3 * allPixelsCount;
for (int pixel : bitmapPixels) {
int r = Color.red(pixel);
int g = Color.green(pixel);
int b = Color.blue(pixel);
for (int x = 0; x < bitmapWidth; x++) {
for (int y = 0; y < bitmapHeight; y++) {
int pixel = bitmap.getPixel(x, y);
int r = Color.red(pixel);
int g = Color.green(pixel);
int b = Color.blue(pixel);
int secondMax = r>g ? r:g;
double max = (secondMax>b ? secondMax:b)/255.0;
int secondMax = r > g ? r : g;
double max = (secondMax > b ? secondMax : b) / 255.0;
int secondMin = r<g ? r:g;
double min = (secondMin<b ? secondMin:b)/255.0;
int secondMin = r < g ? r : g;
double min = (secondMin < b ? secondMin : b) / 255.0;
double luminance = ((max+min)/2.0)*100;
double luminance = ((max + min) / 2.0) * 100;
int highBrightnessLuminance = 40;
int mediumBrightnessLuminance = 26;
int highBrightnessLuminance = 40;
int mediumBrightnessLuminance = 26;
if (luminance<highBrightnessLuminance){
if (luminance>mediumBrightnessLuminance){
numberOfMediumBrightnessPixels++;
if (luminance < highBrightnessLuminance) {
if (luminance > mediumBrightnessLuminance) {
numberOfMediumBrightnessPixels++;
}
} else {
numberOfBrightPixels++;
}
if (numberOfBrightPixels >= brightPixelThreshold || numberOfMediumBrightnessPixels >= mediumBrightPixelThreshold) {
return false;
}
}
else {
numberOfBrightPixels++;
}
if (numberOfBrightPixels>=brightPixelThreshold || numberOfMediumBrightnessPixels>=mediumBrightPixelThreshold){
return false;
}
}
return true;
}

View file

@ -5,6 +5,8 @@ import android.graphics.BitmapRegionDecoder;
import javax.inject.Inject;
import javax.inject.Singleton;
import io.reactivex.Single;
import io.reactivex.schedulers.Schedulers;
import fr.free.nrw.commons.location.LatLng;
import static fr.free.nrw.commons.utils.ImageUtils.*;
@ -17,11 +19,18 @@ public class ImageUtilsWrapper {
}
public @Result int checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
return ImageUtils.checkIfImageIsTooDark(bitmapRegionDecoder);
public Single<Integer> checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
int isImageDark = ImageUtils.checkIfImageIsTooDark(bitmapRegionDecoder);
return Single.just(isImageDark)
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation());
}
public boolean checkImageGeolocationIsDifferent(String geolocationOfFileString, LatLng latLng) {
return ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng);
public Single<Integer> checkImageGeolocationIsDifferent(String geolocationOfFileString, LatLng latLng) {
boolean isImageGeoLocationDifferent = ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng);
return Single.just(isImageGeoLocationDifferent)
.subscribeOn(Schedulers.computation())
.observeOn(Schedulers.computation())
.map(isDifferent -> isDifferent ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT : ImageUtils.IMAGE_OK);
}
}