Merge branch 'master' into 1130(b)

This commit is contained in:
Hassan 2018-02-20 20:53:13 -06:00 committed by GitHub
commit 03015691c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 497 additions and 72 deletions

View file

@ -197,6 +197,10 @@ public class Contribution extends Media {
this.localUri = localUri;
}
public void setDecimalCoords(String decimalCoords) {
this.decimalCoords = decimalCoords;
}
@NonNull
private String licenseTemplateFor(String license) {
switch (license) {
@ -215,6 +219,7 @@ public class Contribution extends Media {
case Prefs.Licenses.CC_BY_SA:
return "{{self|cc-by-sa-3.0}}";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
}

View file

@ -0,0 +1,59 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import android.graphics.BitmapRegionDecoder;
import android.net.Uri;
import android.os.AsyncTask;
import java.io.IOException;
import fr.free.nrw.commons.utils.ImageUtils;
import timber.log.Timber;
/**
* Created by bluesir9 on 16/9/17.
*
* <p>Responsible for checking if the picture that the user is trying to upload is useful or not. Will attempt to filter
* away completely black,fuzzy/blurry pictures(for now).
*
* <p>todo: Detect selfies?
*/
public class DetectUnwantedPicturesAsync extends AsyncTask<Void, Void, ImageUtils.Result> {
interface Callback {
void onResult(ImageUtils.Result result);
}
private final Callback callback;
private final String imageMediaFilePath;
DetectUnwantedPicturesAsync(String imageMediaFilePath, Callback callback) {
this.callback = callback;
this.imageMediaFilePath = imageMediaFilePath;
}
@Override
protected ImageUtils.Result doInBackground(Void... voids) {
try {
Timber.d("FilePath: " + imageMediaFilePath);
if (imageMediaFilePath == null) {
return ImageUtils.Result.IMAGE_OK;
}
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(imageMediaFilePath,false);
return ImageUtils.checkIfImageIsTooDark(decoder);
} catch (IOException ioe) {
Timber.e(ioe, "IO Exception");
return ImageUtils.Result.IMAGE_OK;
}
}
@Override
protected void onPostExecute(ImageUtils.Result result) {
super.onPostExecute(result);
//callback to UI so that it can take necessary decision based on the result obtained
callback.onResult(result);
}
}

View file

@ -3,20 +3,25 @@ package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.content.ContentUris;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.preference.PreferenceManager;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.Date;
import timber.log.Timber;
@ -36,6 +41,7 @@ public class FileUtils {
@Nullable
public static String getPath(Context context, Uri uri) {
String returnPath = null;
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
@ -47,7 +53,7 @@ public class FileUtils {
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
returnPath = Environment.getExternalStorageDirectory() + "/" + split[1];
}
} else if (isDownloadsDocument(uri)) { // DownloadsProvider
@ -55,8 +61,9 @@ public class FileUtils {
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
returnPath = getDataColumn(context, contentUri, null, null);
} else if (isMediaDocument(uri)) { // MediaProvider
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
@ -81,16 +88,55 @@ public class FileUtils {
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
returnPath = getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(context, uri, null, null);
returnPath = getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
returnPath = uri.getPath();
}
if(returnPath == null) {
//fetching path may fail depending on the source URI and all hope is lost
//so we will create and use a copy of the file, which seems to work
String copyPath = null;
try {
ParcelFileDescriptor descriptor
= context.getContentResolver().openFileDescriptor(uri, "r");
if (descriptor != null) {
SharedPreferences sharedPref = PreferenceManager
.getDefaultSharedPreferences(context);
boolean useExtStorage = sharedPref.getBoolean("useExternalStorage", true);
if (useExtStorage) {
copyPath = Environment.getExternalStorageDirectory().toString()
+ "/CommonsApp/" + new Date().getTime() + ".jpg";
File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
newFile.mkdir();
FileUtils.copy(
descriptor.getFileDescriptor(),
copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
copyPath = context.getCacheDir().getAbsolutePath()
+ "/" + new Date().getTime() + ".jpg";
FileUtils.copy(
descriptor.getFileDescriptor(),
copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
} catch (IOException e) {
Timber.w(e, "Error in file " + copyPath);
return null;
}
} else {
return returnPath;
}
return null;
@ -111,7 +157,7 @@ public class FileUtils {
String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String column = MediaStore.Images.ImageColumns.DATA;
final String[] projection = {
column
};

View file

@ -11,7 +11,9 @@ import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
@ -21,6 +23,7 @@ import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.Toast;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
@ -69,6 +72,8 @@ public class MultipleShareActivity extends AuthenticatedActivity
private MediaDetailPagerFragment mediaDetails;
private CategorizationFragment categorizationFragment;
private boolean locationPermitted = false;
@Override
public Media getMediaAtPosition(int i) {
return photosList.get(i);
@ -213,6 +218,14 @@ public class MultipleShareActivity extends AuthenticatedActivity
getSupportFragmentManager().addOnBackStackChangedListener(this);
requestAuthToken();
//TODO: 15/10/17 should location permission be explicitly requested if not provided?
//check if location permission is enabled
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
locationPermitted = true;
}
}
}
@Override
@ -258,6 +271,11 @@ public class MultipleShareActivity extends AuthenticatedActivity
up.setTag("sequence", i);
up.setSource(Contribution.SOURCE_EXTERNAL);
up.setMultiple(true);
String imageGpsCoordinates = extractImageGpsData(uri);
if (imageGpsCoordinates != null) {
Timber.d("GPS data for image found!");
up.setDecimalCoords(imageGpsCoordinates);
}
photosList.add(up);
}
}
@ -287,4 +305,46 @@ public class MultipleShareActivity extends AuthenticatedActivity
getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible());
}
/**
* Will attempt to extract the gps coordinates using exif data or by using the current
* location if available for the image who's imageUri has been provided.
* @param imageUri The uri of the image who's GPS coordinates data we wish to extract
* @return GPS coordinates as a String as is returned by {@link GPSExtractor}
*/
@Nullable
private String extractImageGpsData(Uri imageUri) {
Timber.d("Entering extractImagesGpsData");
if (imageUri == null) {
//now why would you do that???
return null;
}
GPSExtractor gpsExtractor = null;
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(imageUri,"r");
if (fd != null) {
gpsExtractor = new GPSExtractor(fd.getFileDescriptor(),this,prefs);
}
} else {
String filePath = FileUtils.getPath(this,imageUri);
if (filePath != null) {
gpsExtractor = new GPSExtractor(filePath,this,prefs);
}
}
if (gpsExtractor != null) {
//get image coordinates from exif data or user location
return gpsExtractor.getCoords(locationPermitted);
}
} catch (FileNotFoundException fnfe) {
Timber.w(fnfe);
return null;
}
return null;
}
}

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.upload;
import android.Manifest;
import android.content.ContentResolver;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
@ -18,6 +19,7 @@ import android.support.graphics.drawable.VectorDrawableCompat;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
@ -47,11 +49,14 @@ import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.modifications.CategoryModifier;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
import fr.free.nrw.commons.modifications.ModifierSequence;
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.EventLog;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber;
@ -287,7 +292,7 @@ public class ShareActivity
REQUEST_PERM_ON_CREATE_LOCATION);
}
}
performPreuploadProcessingOfFile();
performPreUploadProcessingOfFile();
SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView");
@ -311,7 +316,7 @@ public class ShareActivity
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
backgroundImageView.setImageURI(mediaUri);
storagePermitted = true;
performPreuploadProcessingOfFile();
performPreUploadProcessingOfFile();
}
return;
}
@ -319,7 +324,7 @@ public class ShareActivity
if (grantResults.length >= 1
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
locationPermitted = true;
performPreuploadProcessingOfFile();
performPreUploadProcessingOfFile();
}
return;
}
@ -328,12 +333,12 @@ public class ShareActivity
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
backgroundImageView.setImageURI(mediaUri);
storagePermitted = true;
performPreuploadProcessingOfFile();
performPreUploadProcessingOfFile();
}
if (grantResults.length >= 2
&& grantResults[1] == PackageManager.PERMISSION_GRANTED) {
locationPermitted = true;
performPreuploadProcessingOfFile();
performPreUploadProcessingOfFile();
}
return;
}
@ -344,7 +349,7 @@ public class ShareActivity
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//It is OK to call this at both (1) and (4) because if perm had been granted at
//snackbar, user should not be prompted at submit button
performPreuploadProcessingOfFile();
performPreUploadProcessingOfFile();
//Uploading only begins if storage permission granted from arrow icon
uploadBegins();
@ -355,7 +360,7 @@ public class ShareActivity
}
}
private void performPreuploadProcessingOfFile() {
private void performPreUploadProcessingOfFile() {
if (!useNewPermissions || storagePermitted) {
if (!duplicateCheckPassed) {
//Test SHA1 of image to see if it matches SHA1 of a file on Commons
@ -370,7 +375,17 @@ public class ShareActivity
Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
duplicateCheckPassed = (result == DUPLICATE_PROCEED
|| result == NO_DUPLICATE);
}, mwApi);
/*
TODO: 16/9/17 should we run DetectUnwantedPicturesAsync if DUPLICATE_PROCEED is returned? Since that means
we are processing images that are already on server???...
*/
if (duplicateCheckPassed) {
//image can be uploaded, so now check if its a useless picture or not
performUnwantedPictureDetectionProcess();
}
},mwApi);
fileAsyncTask.execute();
} catch (IOException e) {
Timber.d(e, "IO Exception: ");
@ -384,6 +399,37 @@ public class ShareActivity
}
}
private void performUnwantedPictureDetectionProcess() {
String imageMediaFilePath = FileUtils.getPath(this,mediaUri);
DetectUnwantedPicturesAsync detectUnwantedPicturesAsync = new DetectUnwantedPicturesAsync(imageMediaFilePath, result -> {
if (result != ImageUtils.Result.IMAGE_OK) {
//show appropriate error message
String errorMessage = result == ImageUtils.Result.IMAGE_DARK ? getString(R.string.upload_image_too_dark) : getString(R.string.upload_image_blurry);
AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(this);
errorDialogBuilder.setMessage(errorMessage);
errorDialogBuilder.setTitle(getString(R.string.warning));
errorDialogBuilder.setPositiveButton(getString(R.string.no), (dialogInterface, i) -> {
//user does not wish to upload the picture, take them back to ContributionsActivity
Intent intent = new Intent(ShareActivity.this, ContributionsActivity.class);
dialogInterface.dismiss();
startActivity(intent);
});
errorDialogBuilder.setNegativeButton(getString(R.string.yes), (dialogInterface, i) -> {
//user wishes to go ahead with the upload of this picture, just dismiss this dialog
dialogInterface.dismiss();
});
AlertDialog errorDialog = errorDialogBuilder.create();
if (!isFinishing()) {
errorDialog.show();
}
}
});
detectUnwantedPicturesAsync.execute();
}
private Snackbar requestPermissionUsingSnackBar(String rationale,
final String[] perms,
final int code) {

View file

@ -0,0 +1,135 @@
package fr.free.nrw.commons.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapRegionDecoder;
import android.graphics.Color;
import android.graphics.Rect;
import timber.log.Timber;
/**
* Created by bluesir9 on 3/10/17.
*/
public class ImageUtils {
//atleast 50% of the image in question should be considered dark for the entire image to be dark
private static final double MINIMUM_DARKNESS_FACTOR = 0.50;
//atleast 50% of the image in question should be considered blurry for the entire image to be blurry
private static final double MINIMUM_BLURRYNESS_FACTOR = 0.50;
private static final int LAPLACIAN_VARIANCE_THRESHOLD = 70;
public enum Result {
IMAGE_DARK,
IMAGE_OK
}
/**
* BitmapRegionDecoder allows us to process a large bitmap by breaking it down into smaller rectangles. The rectangles
* are obtained by setting an initial width, height and start position of the rectangle as a factor of the width and
* height of the original bitmap and then manipulating the width, height and position to loop over the entire original
* bitmap. Each individual rectangle is independently processed to check if its too dark. Based on
* the factor of "bright enough" individual rectangles amongst the total rectangles into which the image
* was divided, we will declare the image as wanted/unwanted
*
* @param bitmapRegionDecoder BitmapRegionDecoder for the image we wish to process
* @return Result.IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
* Result.IMAGE_DARK if image is too dark
*/
public static Result checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
if (bitmapRegionDecoder == null) {
Timber.e("Expected bitmapRegionDecoder was null");
return Result.IMAGE_OK;
}
int loadImageHeight = bitmapRegionDecoder.getHeight();
int loadImageWidth = bitmapRegionDecoder.getWidth();
int checkImageTopPosition = 0;
int checkImageBottomPosition = loadImageHeight / 10;
int checkImageLeftPosition = 0;
int checkImageRightPosition = loadImageWidth / 10;
int totalDividedRectangles = 0;
int numberOfDarkRectangles = 0;
while ((checkImageRightPosition <= loadImageWidth) && (checkImageLeftPosition < checkImageRightPosition)) {
while ((checkImageBottomPosition <= loadImageHeight) && (checkImageTopPosition < checkImageBottomPosition)) {
Timber.d("left: " + checkImageLeftPosition + " right: " + checkImageRightPosition + " top: " + checkImageTopPosition + " bottom: " + checkImageBottomPosition);
Rect rect = new Rect(checkImageLeftPosition,checkImageTopPosition,checkImageRightPosition,checkImageBottomPosition);
totalDividedRectangles++;
Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect,null);
if (checkIfImageIsDark(processBitmap)) {
numberOfDarkRectangles++;
}
checkImageTopPosition = checkImageBottomPosition;
checkImageBottomPosition += (checkImageBottomPosition < (loadImageHeight - checkImageBottomPosition)) ? checkImageBottomPosition : (loadImageHeight - checkImageBottomPosition);
}
checkImageTopPosition = 0; //reset to start
checkImageBottomPosition = loadImageHeight / 10; //reset to start
checkImageLeftPosition = checkImageRightPosition;
checkImageRightPosition += (checkImageRightPosition < (loadImageWidth - checkImageRightPosition)) ? checkImageRightPosition : (loadImageWidth - checkImageRightPosition);
}
Timber.d("dark rectangles count = " + numberOfDarkRectangles + ", total rectangles count = " + totalDividedRectangles);
if (numberOfDarkRectangles > totalDividedRectangles * MINIMUM_DARKNESS_FACTOR) {
return Result.IMAGE_DARK;
}
return Result.IMAGE_OK;
}
/**
* Pulls the pixels into an array and then runs through it while checking the brightness of each pixel.
* The calculation of brightness of each pixel is done by extracting the RGB constituents of the pixel
* and then applying the formula to calculate its "Luminance". If this brightness value is less than
* 50 then the pixel is considered to be dark. Based on the MINIMUM_DARKNESS_FACTOR if enough pixels
* are dark then the entire bitmap is considered to be dark.
*
* <p>For more information on this brightness/darkness calculation technique refer the accepted answer
* on this -> https://stackoverflow.com/questions/35914461/how-to-detect-dark-photos-in-android/35914745
* SO question and follow the trail.
*
* @param bitmap The bitmap that needs to be checked.
* @return true if bitmap is dark or null, false if bitmap is bright
*/
private static boolean checkIfImageIsDark(Bitmap bitmap) {
if (bitmap == null) {
Timber.e("Expected bitmap was null");
return true;
}
int bitmapWidth = bitmap.getWidth();
int bitmapHeight = bitmap.getHeight();
int allPixelsCount = bitmapWidth * bitmapHeight;
int[] bitmapPixels = new int[allPixelsCount];
bitmap.getPixels(bitmapPixels,0,bitmapWidth,0,0,bitmapWidth,bitmapHeight);
boolean isImageDark = false;
int darkPixelsCount = 0;
for (int pixel : bitmapPixels) {
int r = Color.red(pixel);
int g = Color.green(pixel);
int b = Color.blue(pixel);
int brightness = (int) (0.2126 * r + 0.7152 * g + 0.0722 * b);
if (brightness < 50) {
//pixel is dark
darkPixelsCount++;
if (darkPixelsCount > allPixelsCount * MINIMUM_DARKNESS_FACTOR) {
isImageDark = true;
break;
}
}
}
return isImageDark;
}
}