mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-29 05:43:55 +01:00
Merge branch 'master' into 1130(j)
This commit is contained in:
commit
14686138df
64 changed files with 844 additions and 156 deletions
|
|
@ -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\\"));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
135
app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java
Normal file
135
app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue