Merge branch 'master' into 1130(j)

This commit is contained in:
Hassan 2018-02-25 22:04:29 -06:00 committed by GitHub
commit 14686138df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 844 additions and 156 deletions

View file

@ -3,6 +3,8 @@ package fr.free.nrw.commons;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.customtabs.CustomTabsIntent;
import android.support.v4.content.ContextCompat;
import android.view.View;
import android.widget.TextView;
@ -47,22 +49,29 @@ public class AboutActivity extends NavigationBaseActivity {
intent.setPackage("com.facebook.katana");
startActivity(intent);
} catch (Exception e) {
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://www.facebook.com/" + "1921335171459985")));
Utils.handleWebUrl(this,Uri.parse("https://www.facebook.com/" + "1921335171459985"));
}
}
@OnClick(R.id.github_launch_icon)
public void launchGithub(View view) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/commons-app/apps-android-commons\\"));
startActivity(browserIntent);
Utils.handleWebUrl(this,Uri.parse("https://commons-app.github.io/\\"));
}
@OnClick(R.id.website_launch_icon)
public void launchWebsite(View view) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://commons-app.github.io/\\"));
startActivity(browserIntent);
Utils.handleWebUrl(this,Uri.parse("https://commons-app.github.io/\\"));
}
@OnClick(R.id.about_credits)
public void launchCredits(View view) {
Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/blob/master/CREDITS/\\"));
}
@OnClick(R.id.about_privacy_policy)
public void launchPrivacyPolicy(View view) {
Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\\"));
}
}

View file

@ -1,8 +1,12 @@
package fr.free.nrw.commons;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.customtabs.CustomTabsIntent;
import android.support.v4.content.ContextCompat;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
@ -11,6 +15,7 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Locale;
import java.util.regex.Matcher;
@ -159,4 +164,15 @@ public class Utils {
return stringBuilder.toString();
}
public static void handleWebUrl(Context context,Uri url){
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor(ContextCompat.getColor(context, R.color.primaryColor));
builder.setSecondaryToolbarColor(ContextCompat.getColor(context, R.color.primaryDarkColor));
builder.setExitAnimations(context, android.R.anim.slide_in_left, android.R.anim.slide_out_right);
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
customTabsIntent.launchUrl(context, url);
}
}

View file

@ -4,6 +4,7 @@ import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.content.SharedPreferences;
@ -17,10 +18,12 @@ import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatDelegate;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
@ -69,6 +72,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
@BindView(R.id.loginTwoFactor) EditText twoFactorEdit;
@BindView(R.id.error_message_container) ViewGroup errorMessageContainer;
@BindView(R.id.error_message) TextView errorMessage;
@BindView(R.id.login_credentials) TextView loginCredentials;
@BindView(R.id.two_factor_container)TextInputLayout twoFactorContainer;
ProgressDialog progressDialog;
private AppCompatDelegate delegate;
@ -91,14 +95,39 @@ public class LoginActivity extends AccountAuthenticatorActivity {
ButterKnife.bind(this);
usernameEdit.addTextChangedListener(textWatcher);
usernameEdit.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
hideKeyboard(v);
}
});
passwordEdit.addTextChangedListener(textWatcher);
passwordEdit.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
hideKeyboard(v);
}
});
twoFactorEdit.addTextChangedListener(textWatcher);
passwordEdit.setOnEditorActionListener(newLoginInputActionListener());
loginButton.setOnClickListener(view -> performLogin());
signupButton.setOnClickListener(view -> signUp());
if(BuildConfig.FLAVOR == "beta"){
loginCredentials.setText(getString(R.string.login_credential));
} else {
loginCredentials.setVisibility(View.GONE);
}
}
public void hideKeyboard(View view) {
InputMethodManager inputMethodManager =(InputMethodManager)this.getSystemService(Activity.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);

View file

@ -1,17 +1,22 @@
package fr.free.nrw.commons.category;
import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
@ -37,6 +42,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.upload.MwVolleyApi;
import fr.free.nrw.commons.upload.SingleUploadFragment;
import fr.free.nrw.commons.utils.StringSortingUtils;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -72,6 +78,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
private OnCategoriesSaveHandler onCategoriesSaveHandler;
private HashMap<String, ArrayList<String>> categoriesCache;
private List<CategoryItem> selectedCategories = new ArrayList<>();
private TitleTextWatcher textWatcher = new TitleTextWatcher();
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
if (item.isSelected()) {
@ -102,6 +109,15 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
categoriesAdapter = adapterFactory.create(items);
categoriesList.setAdapter(categoriesAdapter);
categoriesFilter.addTextChangedListener(textWatcher);
categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
hideKeyboard(v);
}
});
RxTextView.textChanges(categoriesFilter)
.takeUntil(RxView.detaches(categoriesFilter))
.debounce(500, TimeUnit.MILLISECONDS)
@ -110,6 +126,18 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
return rootView;
}
public void hideKeyboard(View view) {
InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
@Override
public void onDestroyView() {
categoriesFilter.removeTextChangedListener(textWatcher);
super.onDestroyView();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
menu.clear();
@ -351,4 +379,21 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
.create()
.show();
}
private class TitleTextWatcher implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
}
@Override
public void afterTextChanged(Editable editable) {
if (getActivity() != null) {
getActivity().invalidateOptionsMenu();
}
}
}
}

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

@ -10,6 +10,7 @@ import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AlertDialog;
import android.view.Menu;
import android.view.MenuInflater;
@ -60,7 +61,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
private NearbyActivityMode viewMode;
private Disposable placesDisposable;
private boolean lockNearbyView; //Determines if the nearby places needs to be refreshed
@BindView(R.id.swipe_container) SwipeRefreshLayout swipeLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -70,6 +71,13 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
bundle = new Bundle();
initDrawer();
initViewState();
swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
lockNearbyView(false);
refreshView(true);
}
});
}
private void initViewState() {
@ -309,7 +317,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
} else {
setListFragment();
}
swipeLayout.setRefreshing(false);
hideProgressBar();
}

View file

@ -3,7 +3,9 @@ package fr.free.nrw.commons.nearby;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.customtabs.CustomTabsIntent;
import android.support.v4.app.FragmentActivity;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.PopupMenu;
import android.view.LayoutInflater;
import android.view.MenuItem;
@ -17,6 +19,7 @@ import butterknife.ButterKnife;
import butterknife.OnClick;
import butterknife.Unbinder;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.ui.widget.OverlayDialog;
import fr.free.nrw.commons.utils.DialogUtil;
@ -141,8 +144,7 @@ public class NearbyInfoDialog extends OverlayDialog {
}
private void openWebView(Uri link) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, link);
startActivity(browserIntent);
Utils.handleWebUrl(getContext(),link);
}
@OnClick(R.id.emptyLayout)

View file

@ -10,6 +10,7 @@ import android.support.v7.widget.RecyclerView;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
@ -58,6 +59,7 @@ public class NotificationActivity extends NavigationBaseActivity {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(notificationList -> {
Collections.reverse(notificationList);
Timber.d("Number of notifications is %d", notificationList.size());
setAdapter(notificationList);
}, throwable -> Timber.e(throwable, "Error occurred while loading notifications"));

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

@ -1,5 +1,6 @@
package fr.free.nrw.commons.upload;
import android.app.Activity;
import android.content.Context;
import android.graphics.Point;
import android.net.Uri;
@ -10,6 +11,7 @@ import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@ -177,9 +179,21 @@ public class MultipleUploadListFragment extends Fragment {
photosGrid.setColumnWidth(photoSize.x);
baseTitle.addTextChangedListener(textWatcher);
baseTitle.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
hideKeyboard(v);
}
});
return view;
}
public void hideKeyboard(View view) {
InputMethodManager inputMethodManager =(InputMethodManager)getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
}
@Override
public void onDestroyView() {
baseTitle.removeTextChangedListener(textWatcher);

View file

@ -4,6 +4,7 @@ import android.Manifest;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
@ -20,6 +21,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;
@ -50,11 +52,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;
@ -290,7 +295,7 @@ public class ShareActivity
REQUEST_PERM_ON_CREATE_LOCATION);
}
}
performPreuploadProcessingOfFile();
performPreUploadProcessingOfFile();
SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView");
@ -314,7 +319,7 @@ public class ShareActivity
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
backgroundImageView.setImageURI(mediaUri);
storagePermitted = true;
performPreuploadProcessingOfFile();
performPreUploadProcessingOfFile();
}
return;
}
@ -322,7 +327,7 @@ public class ShareActivity
if (grantResults.length >= 1
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
locationPermitted = true;
performPreuploadProcessingOfFile();
performPreUploadProcessingOfFile();
}
return;
}
@ -331,12 +336,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;
}
@ -347,7 +352,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();
@ -358,7 +363,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
@ -373,7 +378,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: ");
@ -387,6 +402,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;
}
}