diff --git a/.gitignore b/.gitignore index 1ab05305e..5514c2b09 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,10 @@ app/gradle/wrapper/gradle-wrapper.jar app/gradlew app/gradlew.bat app/gradle/wrapper/gradle-wrapper.properties + +#related to OpenCV +/libraries/opencv/build +app/src/main/jniLibs +#Below removes all the HTML files related to OpenCV documentation. The documentation can be otherwise found at: +#https://docs.opencv.org/3.3.0/ +/libraries/opencv/javadoc/ diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..f58c9225b --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +## Description + +Fixes #{GitHub issue number} + +{Describe the changes made and why they were made.} + +## Screenshots showing what changed + +{Only for user interface changes, otherwise remove this section. See [how to take a screenshot](https://android.stackexchange.com/questions/1759/how-to-take-a-screenshot-with-an-android-device)} diff --git a/app/build.gradle b/app/build.gradle index 98693126e..6f225dc2f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,7 +18,7 @@ dependencies { implementation 'com.google.code.gson:gson:2.8.1' implementation 'com.jakewharton.timber:timber:4.5.1' implementation 'info.debatty:java-string-similarity:0.24' - implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.2.1@aar'){ + implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.4.1@aar'){ transitive=true } @@ -26,6 +26,7 @@ dependencies { implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION" implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION" implementation "com.android.support:design:$SUPPORT_LIB_VERSION" + implementation "com.android.support:customtabs:$SUPPORT_LIB_VERSION" implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION" @@ -38,6 +39,26 @@ dependencies { implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' // Because RxAndroid releases are few and far between, it is recommended you also // explicitly depend on RxJava's latest version for bug fixes and new features. + compile 'io.reactivex.rxjava2:rxjava:2.1.2' + compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0' + compile 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0' + compile 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0' + compile 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0' + + compile 'com.facebook.fresco:fresco:1.3.0' + compile 'com.facebook.stetho:stetho:1.5.0' + + testCompile 'junit:junit:4.12' + testCompile 'org.robolectric:robolectric:3.7.1' + + testCompile 'com.squareup.okhttp3:mockwebserver:3.8.1' + androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.8.1' + androidTestCompile "com.android.support:support-annotations:${project.SUPPORT_LIB_VERSION}" + androidTestCompile 'com.android.support.test.espresso:espresso-core:3.0.1' + + debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1' + releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' + testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' implementation 'io.reactivex.rxjava2:rxjava:2.1.2' implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0' @@ -57,7 +78,7 @@ dependencies { androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" testImplementation 'junit:junit:4.12' - testImplementation 'org.robolectric:robolectric:3.4' + testImplementation 'org.robolectric:robolectric:3.7.1' testImplementation 'org.mockito:mockito-all:1.10.19' testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' @@ -91,6 +112,8 @@ android { targetSdkVersion project.targetSdkVersion testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true + + multiDexEnabled true } sourceSets { diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java index 260878b00..27816d762 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java @@ -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; @@ -31,7 +33,7 @@ public class AboutActivity extends NavigationBaseActivity { ButterKnife.bind(this); - String aboutText = getString(R.string.about_license, getString(R.string.trademarked_name)); + String aboutText = getString(R.string.about_license); aboutLicenseText.setHtmlText(aboutText); versionText.setText(BuildConfig.VERSION_NAME); @@ -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\\")); + } + + } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java index 0927f0338..25e778b74 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java @@ -283,7 +283,7 @@ public class MediaDataExtractor { /** * Take our metadata and inject it into a live Media object. * Media object might contain stale or cached data, or emptiness. - * @param media + * @param media Media object to inject into */ public void fill(Media media) { if (!fetched) { diff --git a/app/src/main/java/fr/free/nrw/commons/Utils.java b/app/src/main/java/fr/free/nrw/commons/Utils.java index 967f2cf8e..9f91bff12 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -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; @@ -65,7 +70,7 @@ public class Utils { /** * Capitalizes the first character of a string. * - * @param string + * @param string String to alter * @return string with capitalized first character */ public static String capitalize(String string) { @@ -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); + } + } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index c5707b970..cbdff2e0b 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -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; @@ -32,7 +35,6 @@ import javax.inject.Named; import butterknife.BindView; import butterknife.ButterKnife; -import dagger.android.AndroidInjection; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.PageTitle; import fr.free.nrw.commons.R; @@ -70,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; @@ -92,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); diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java deleted file mode 100644 index b751dfe93..000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java +++ /dev/null @@ -1,128 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.AccountAuthenticatorResponse; -import android.app.ProgressDialog; -import android.content.SharedPreferences; -import android.os.AsyncTask; -import android.os.Bundle; - -import java.io.IOException; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.mwapi.MediaWikiApi; -import timber.log.Timber; - -import static android.accounts.AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE; -import static android.accounts.AccountManager.KEY_ACCOUNT_NAME; -import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE; -import static fr.free.nrw.commons.auth.AccountUtil.ACCOUNT_TYPE; - -class LoginTask extends AsyncTask { - - private LoginActivity loginActivity; - private String username; - private String password; - private String twoFactorCode = ""; - private AccountUtil accountUtil; - private MediaWikiApi mwApi; - - public LoginTask(LoginActivity loginActivity, String username, String password, - String twoFactorCode, AccountUtil accountUtil, - MediaWikiApi mwApi, SharedPreferences prefs) { - this.loginActivity = loginActivity; - this.username = username; - this.password = password; - this.twoFactorCode = twoFactorCode; - this.accountUtil = accountUtil; - this.mwApi = mwApi; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - loginActivity.progressDialog = new ProgressDialog(loginActivity); - loginActivity.progressDialog.setIndeterminate(true); - loginActivity.progressDialog.setTitle(loginActivity.getString(R.string.logging_in_title)); - loginActivity.progressDialog.setMessage(loginActivity.getString(R.string.logging_in_message)); - loginActivity.progressDialog.setCanceledOnTouchOutside(false); - loginActivity.progressDialog.show(); - } - - @Override - protected String doInBackground(String... params) { - try { - if (twoFactorCode.isEmpty()) { - return mwApi.login(username, password); - } else { - return mwApi.login(username, password, twoFactorCode); - } - } catch (IOException e) { - // Do something better! - return "NetworkFailure"; - } - } - - @Override - protected void onPostExecute(String result) { - super.onPostExecute(result); - Timber.d("Login done!"); - - if (result.equals("PASS")) { - handlePassResult(); - } else { - handleOtherResults(result); - } - } - - private void handlePassResult() { - loginActivity.showSuccessAndDismissDialog(); - - AccountAuthenticatorResponse response = null; - - Bundle extras = loginActivity.getIntent().getExtras(); - if (extras != null) { - Timber.d("Bundle of extras: %s", extras); - response = extras.getParcelable(KEY_ACCOUNT_AUTHENTICATOR_RESPONSE); - if (response != null) { - Bundle authResult = new Bundle(); - authResult.putString(KEY_ACCOUNT_NAME, username); - authResult.putString(KEY_ACCOUNT_TYPE, ACCOUNT_TYPE); - response.onResult(authResult); - } - } - - accountUtil.createAccount(response, username, password); - loginActivity.startMainActivity(); - } - - /** - * Match known failure message codes and provide messages. - * @param result String - */ - private void handleOtherResults(String result) { - if (result.equals("NetworkFailure")) { - // Matches NetworkFailure which is created by the doInBackground method - loginActivity.showMessageAndCancelDialog(R.string.login_failed_network); - } else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { - // Matches nosuchuser, nosuchusershort, noname - loginActivity.showMessageAndCancelDialog(R.string.login_failed_username); - loginActivity.emptySensitiveEditFields(); - } else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) { - // Matches wrongpassword, wrongpasswordempty - loginActivity.showMessageAndCancelDialog(R.string.login_failed_password); - loginActivity.emptySensitiveEditFields(); - } else if (result.toLowerCase().contains("throttle".toLowerCase())) { - // Matches unknown throttle error codes - loginActivity.showMessageAndCancelDialog(R.string.login_failed_throttled); - } else if (result.toLowerCase().contains("userblocked".toLowerCase())) { - // Matches login-userblocked - loginActivity.showMessageAndCancelDialog(R.string.login_failed_blocked); - } else if (result.equals("2FA")) { - loginActivity.askUserForTwoFactorAuth(); - } else { - // Occurs with unhandled login failure codes - Timber.d("Login failed with reason: %s", result); - loginActivity.showMessageAndCancelDialog(R.string.login_failed_generic); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java index 51376c7fe..c826013f0 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java @@ -1,18 +1,22 @@ package fr.free.nrw.commons.category; -import android.content.Context; +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; @@ -38,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; @@ -73,6 +78,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment { private OnCategoriesSaveHandler onCategoriesSaveHandler; private HashMap> categoriesCache; private List selectedCategories = new ArrayList<>(); + private TitleTextWatcher textWatcher = new TitleTextWatcher(); private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> { if (item.isSelected()) { @@ -103,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) @@ -111,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(); @@ -352,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(); + } + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java index 00baac847..7861f96de 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java @@ -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); } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index a243330c3..db20963e7 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -93,10 +93,15 @@ class ContributionController { shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY); break; case SELECT_FROM_CAMERA: - shareIntent.setType("image/jpeg"); //FIXME: Find out appropriate mime type + //FIXME: Find out appropriate mime type + // AFAIK this is the right type for a JPEG image + // https://developer.android.com/training/sharing/send.html#send-binary-content + shareIntent.setType("image/jpeg"); shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri); shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA); break; + default: + break; } Timber.i("Image selected"); try { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java index 7dc750732..0a68ac626 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsContentProvider.java @@ -86,7 +86,7 @@ public class ContributionsContentProvider extends CommonsDaggerContentProvider { public Uri insert(@NonNull Uri uri, ContentValues contentValues) { int uriType = uriMatcher.match(uri); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - long id = 0; + long id; switch (uriType) { case CONTRIBUTIONS: id = sqlDB.insert(TABLE_NAME, null, contentValues); @@ -158,7 +158,7 @@ public class ContributionsContentProvider extends CommonsDaggerContentProvider { */ int uriType = uriMatcher.match(uri); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - int rowsUpdated = 0; + int rowsUpdated; switch (uriType) { case CONTRIBUTIONS: rowsUpdated = sqlDB.update(TABLE_NAME, contentValues, selection, selectionArgs); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index 3a391b293..25bf6eb93 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -20,6 +20,8 @@ import android.widget.ListAdapter; import android.widget.ProgressBar; import android.widget.TextView; +import java.util.Arrays; + import javax.inject.Inject; import javax.inject.Named; @@ -45,8 +47,12 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { @BindView(R.id.loadingContributionsProgressBar) ProgressBar progressBar; - @Inject @Named("prefs") SharedPreferences prefs; - @Inject @Named("default_preferences") SharedPreferences defaultPrefs; + @Inject + @Named("prefs") + SharedPreferences prefs; + @Inject + @Named("default_preferences") + SharedPreferences defaultPrefs; private ContributionController controller; @@ -208,7 +214,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Timber.d("onRequestPermissionsResult: req code = " + " perm = " - + permissions + " grant =" + grantResults); + + Arrays.toString(permissions) + " grant =" + Arrays.toString(grantResults)); switch (requestCode) { // 1 = Storage allowed when gallery selected diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java index 5be634a2e..aedc3f789 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsSyncAdapter.java @@ -23,7 +23,6 @@ import java.util.TimeZone; import javax.inject.Inject; import javax.inject.Named; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.mwapi.LogEventResult; diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java index 14985c6b6..f5974d519 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -1,7 +1,5 @@ package fr.free.nrw.commons.di; -import android.content.Context; - import javax.inject.Singleton; import dagger.Component; @@ -11,10 +9,7 @@ import dagger.android.support.AndroidSupportInjectionModule; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.MediaWikiImageView; import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.category.CategoryContentProvider; -import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.contributions.ContributionsSyncAdapter; -import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModificationsSyncAdapter; import fr.free.nrw.commons.settings.SettingsFragment; diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java index d949189ed..851114ef9 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java @@ -14,9 +14,6 @@ import android.support.v4.content.ContextCompat; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; -import javax.inject.Inject; -import javax.inject.Singleton; - import timber.log.Timber; public class LocationServiceManager implements LocationListener { @@ -33,6 +30,7 @@ public class LocationServiceManager implements LocationListener { /** * Constructs a new instance of LocationServiceManager. + * * @param context the context */ public LocationServiceManager(Context context) { @@ -42,6 +40,7 @@ public class LocationServiceManager implements LocationListener { /** * Returns the current status of the GPS provider. + * * @return true if the GPS provider is enabled */ public boolean isProviderEnabled() { @@ -50,6 +49,7 @@ public class LocationServiceManager implements LocationListener { /** * Returns whether the location permission is granted. + * * @return true if the location permission is granted */ public boolean isLocationPermissionGranted() { @@ -59,6 +59,7 @@ public class LocationServiceManager implements LocationListener { /** * Requests the location permission to be granted. + * * @param activity the activity */ public void requestPermissions(Activity activity) { @@ -71,11 +72,9 @@ public class LocationServiceManager implements LocationListener { } public boolean isPermissionExplanationRequired(Activity activity) { - if (activity.isFinishing()) { - return false; - } - return ActivityCompat.shouldShowRequestPermissionRationale(activity, - Manifest.permission.ACCESS_FINE_LOCATION); + return !activity.isFinishing() && + ActivityCompat.shouldShowRequestPermissionRationale(activity, + Manifest.permission.ACCESS_FINE_LOCATION); } public LatLng getLastLocation() { @@ -85,7 +84,8 @@ public class LocationServiceManager implements LocationListener { return LatLng.from(lastLocation); } - /** Registers a LocationManager to listen for current location. + /** + * Registers a LocationManager to listen for current location. */ public void registerLocationManager() { if (!isLocationManagerRegistered) @@ -95,6 +95,7 @@ public class LocationServiceManager implements LocationListener { /** * Requests location updates from the specified provider. + * * @param locationProvider the location provider * @return true if successful */ @@ -116,7 +117,8 @@ public class LocationServiceManager implements LocationListener { /** * Returns whether a given location is better than the current best location. - * @param location the location to be tested + * + * @param location the location to be tested * @param currentBestLocation the current best location * @return true if the given location is better */ @@ -172,7 +174,8 @@ public class LocationServiceManager implements LocationListener { return provider1.equals(provider2); } - /** Unregisters location manager. + /** + * Unregisters location manager. */ public void unregisterLocationManager() { isLocationManagerRegistered = false; @@ -185,6 +188,7 @@ public class LocationServiceManager implements LocationListener { /** * Adds a new listener to the list of location listeners. + * * @param listener the new listener */ public void addLocationListener(LocationUpdateListener listener) { @@ -195,6 +199,7 @@ public class LocationServiceManager implements LocationListener { /** * Removes a listener from the list of location listeners. + * * @param listener the listener to be removed */ public void removeLocationListener(LocationUpdateListener listener) { diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 60577ead7..70bf281dc 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -14,6 +14,7 @@ import android.view.ViewTreeObserver; import android.widget.LinearLayout; import android.widget.ScrollView; import android.widget.TextView; +import android.widget.Toast; import java.io.IOException; import java.text.SimpleDateFormat; @@ -76,7 +77,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once! private ViewTreeObserver.OnScrollChangedListener scrollListener; private DataSetObserver dataObserver; - private AsyncTask detailFetchTask; + private AsyncTask detailFetchTask; private LicenseList licenseList; @Override @@ -95,7 +96,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - detailProvider = (MediaDetailPagerFragment.MediaDetailProvider)getActivity(); + detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity(); if (savedInstanceState != null) { editable = savedInstanceState.getBoolean("editable"); @@ -156,7 +157,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { return view; } - @Override public void onResume() { + @Override + public void onResume() { super.onResume(); Media media = detailProvider.getMediaAtPosition(index); if (media == null) { @@ -238,13 +240,13 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { detailFetchTask.cancel(true); detailFetchTask = null; } - if (layoutListener != null) { + if (layoutListener != null && getView() != null) { getView().getViewTreeObserver().removeGlobalOnLayoutListener(layoutListener); // old Android was on crack. CRACK IS WHACK layoutListener = null; } - if (scrollListener != null) { + if (scrollListener != null && getView() != null) { getView().getViewTreeObserver().removeOnScrollChangedListener(scrollListener); - scrollListener = null; + scrollListener = null; } if (dataObserver != null) { detailProvider.unregisterDataSetObserver(dataObserver); @@ -272,7 +274,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { } private void setOnClickListeners(final Media media) { - license.setOnClickListener(v -> openWebBrowser(licenseLink(media))); + if (licenseLink(media) != null) { + license.setOnClickListener(v -> openWebBrowser(licenseLink(media))); + } else { + Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT); + toast.show(); + } if (media.getCoordinates() != null) { coordinates.setOnClickListener(v -> openMap(media.getCoordinates())); } @@ -289,7 +296,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { private View buildCatLabel(final String catName, ViewGroup categoryContainer) { final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false); - final CompatTextView textView = (CompatTextView)item.findViewById(R.id.mediaDetailCategoryItemText); + final CompatTextView textView = (CompatTextView) item.findViewById(R.id.mediaDetailCategoryItemText); textView.setText(catName); if (categoriesLoaded && categoriesPresent) { @@ -308,7 +315,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { // You must face the darkness alone int scrollY = scrollView.getScrollY(); int scrollMax = getView().getHeight(); - float scrollPercentage = (float)scrollY / (float)scrollMax; + float scrollPercentage = (float) scrollY / (float) scrollMax; final float transparencyMax = 0.75f; if (scrollPercentage > transparencyMax) { scrollPercentage = transparencyMax; @@ -362,7 +369,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { } - private @Nullable String licenseLink(Media media) { + private @Nullable + String licenseLink(Media media) { String licenseKey = media.getLicense(); if (licenseKey == null || licenseKey.equals("")) { return null; diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index d70b515bb..3dd8d69e8 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -43,9 +43,13 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener { - @Inject MediaWikiApi mwApi; - @Inject SessionManager sessionManager; - @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject + MediaWikiApi mwApi; + @Inject + SessionManager sessionManager; + @Inject + @Named("default_preferences") + SharedPreferences prefs; private ViewPager pager; private Boolean editable; @@ -164,13 +168,19 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple req.allowScanningByMediaScanner(); req.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !(ContextCompat.checkSelfPermission(getContext(), READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && + ContextCompat.checkSelfPermission(getContext(), READ_EXTERNAL_STORAGE) + != PERMISSION_GRANTED + && getView() != null) { Snackbar.make(getView(), R.string.read_storage_permission_rationale, Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok, view -> ActivityCompat.requestPermissions(getActivity(), new String[]{READ_EXTERNAL_STORAGE}, 1)).show(); } else { - ((DownloadManager) getActivity().getSystemService(DOWNLOAD_SERVICE)).enqueue(req); + DownloadManager systemService = (DownloadManager) getActivity().getSystemService(DOWNLOAD_SERVICE); + if (systemService != null) { + systemService.enqueue(req); + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java index c7b92a3ec..0d4468d84 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModificationsContentProvider.java @@ -70,7 +70,7 @@ public class ModificationsContentProvider extends CommonsDaggerContentProvider { public Uri insert(@NonNull Uri uri, ContentValues contentValues) { int uriType = uriMatcher.match(uri); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - long id = 0; + long id; switch (uriType) { case MODIFICATIONS: id = sqlDB.insert(TABLE_NAME, null, contentValues); @@ -132,7 +132,7 @@ public class ModificationsContentProvider extends CommonsDaggerContentProvider { */ int uriType = uriMatcher.match(uri); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - int rowsUpdated = 0; + int rowsUpdated; switch (uriType) { case MODIFICATIONS: rowsUpdated = sqlDB.update(TABLE_NAME, diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java index e6b741d7a..a23079b5e 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequenceDao.java @@ -52,7 +52,7 @@ public class ModifierSequenceDao { ModifierSequence fromCursor(Cursor cursor) { // Hardcoding column positions! - ModifierSequence ms = null; + ModifierSequence ms; try { ms = new ModifierSequence(Uri.parse(cursor.getString(1)), new JSONObject(cursor.getString(2))); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java index 57d21dd95..7aecee337 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java @@ -1,6 +1,5 @@ package fr.free.nrw.commons.nearby; -import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; @@ -11,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; @@ -41,8 +41,6 @@ import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; -import static fr.free.nrw.commons.location.LocationServiceManager.LOCATION_REQUEST; - public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener { @@ -63,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); @@ -73,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() { @@ -259,7 +264,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp /** * This method should be the single point to load/refresh nearby places * - * @param isHardRefresh + * @param isHardRefresh Should display a toast if the location hasn't changed */ private void refreshView(boolean isHardRefresh) { if (lockNearbyView) { @@ -312,7 +317,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp } else { setListFragment(); } - + swipeLayout.setRefreshing(false); hideProgressBar(); } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index 624a0de0f..035532c11 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -39,8 +39,9 @@ public class NearbyController { /** * Prepares Place list to make their distance information update later. + * * @param curLatLng current location for user - * @param context context + * @param context context * @return Place list without distance information */ public List loadAttractionsFromLocation(LatLng curLatLng, Context context) { @@ -51,25 +52,24 @@ public class NearbyController { List places = prefs.getBoolean("useWikidata", true) ? nearbyPlaces.getFromWikidataQuery(curLatLng, Locale.getDefault().getLanguage()) : nearbyPlaces.getFromWikiNeedsPictures(); - if (curLatLng != null) { - Timber.d("Sorting places by distance..."); - final Map distances = new HashMap<>(); - for (Place place: places) { - distances.put(place, computeDistanceBetween(place.location, curLatLng)); - } - Collections.sort(places, - (lhs, rhs) -> { - double lhsDistance = distances.get(lhs); - double rhsDistance = distances.get(rhs); - return (int) (lhsDistance - rhsDistance); - } - ); + Timber.d("Sorting places by distance..."); + final Map distances = new HashMap<>(); + for (Place place : places) { + distances.put(place, computeDistanceBetween(place.location, curLatLng)); } + Collections.sort(places, + (lhs, rhs) -> { + double lhsDistance = distances.get(lhs); + double rhsDistance = distances.get(rhs); + return (int) (lhsDistance - rhsDistance); + } + ); return places; } /** * Loads attractions from location for list view, we need to return Place data type. + * * @param curLatLng users current location * @param placeList list of nearby places in Place data type * @return Place list that holds nearby places @@ -78,7 +78,7 @@ public class NearbyController { LatLng curLatLng, List placeList) { placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS)); - for (Place place: placeList) { + for (Place place : placeList) { String distance = formatDistanceBetween(curLatLng, place.location); place.setDistance(distance); } @@ -86,7 +86,8 @@ public class NearbyController { } /** - *Loads attractions from location for map view, we need to return BaseMarkerOption data type. + * Loads attractions from location for map view, we need to return BaseMarkerOption data type. + * * @param curLatLng users current location * @param placeList list of nearby places in Place data type * @return BaseMarkerOptions list that holds nearby places @@ -103,26 +104,28 @@ public class NearbyController { placeList = placeList.subList(0, Math.min(placeList.size(), MAX_RESULTS)); - Bitmap icon = UiUtils.getBitmap( - VectorDrawableCompat.create( - context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme() - )); + VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create( + context.getResources(), R.drawable.ic_custom_map_marker, context.getTheme() + ); + if (vectorDrawable != null) { + Bitmap icon = UiUtils.getBitmap(vectorDrawable); - for (Place place: placeList) { - String distance = formatDistanceBetween(curLatLng, place.location); - place.setDistance(distance); + for (Place place : placeList) { + String distance = formatDistanceBetween(curLatLng, place.location); + place.setDistance(distance); - NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker(); - nearbyBaseMarker.title(place.name); - nearbyBaseMarker.position( - new com.mapbox.mapboxsdk.geometry.LatLng( - place.location.getLatitude(), - place.location.getLongitude())); - nearbyBaseMarker.place(place); - nearbyBaseMarker.icon(IconFactory.getInstance(context) - .fromBitmap(icon)); + NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker(); + nearbyBaseMarker.title(place.name); + nearbyBaseMarker.position( + new com.mapbox.mapboxsdk.geometry.LatLng( + place.location.getLatitude(), + place.location.getLongitude())); + nearbyBaseMarker.place(place); + nearbyBaseMarker.icon(IconFactory.getInstance(context) + .fromBitmap(icon)); - baseMarkerOptions.add(nearbyBaseMarker); + baseMarkerOptions.add(nearbyBaseMarker); + } } return baseMarkerOptions; } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyInfoDialog.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyInfoDialog.java index b383fd9b9..69fce546c 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyInfoDialog.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyInfoDialog.java @@ -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) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java index 3cfb4840f..d57a23137 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java @@ -19,7 +19,6 @@ import java.util.Collections; import java.util.List; import dagger.android.support.AndroidSupportInjection; -import dagger.android.support.DaggerFragment; import fr.free.nrw.commons.R; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.utils.UriDeserializer; diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NoPermissionsFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NoPermissionsFragment.java index 7a907a1e4..f08fa6acd 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NoPermissionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NoPermissionsFragment.java @@ -9,7 +9,6 @@ import android.view.ViewGroup; import butterknife.ButterKnife; import dagger.android.support.AndroidSupportInjection; -import dagger.android.support.DaggerFragment; import fr.free.nrw.commons.R; import timber.log.Timber; diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java index c90e61318..cdb9e97d3 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -5,11 +5,13 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.support.v7.widget.DividerItemDecoration; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import com.pedrogomez.renderers.RVRendererAdapter; +import java.util.Collections; import java.util.List; import javax.inject.Inject; @@ -45,8 +47,9 @@ public class NotificationActivity extends NavigationBaseActivity { } private void initListView() { - recyclerView = findViewById(R.id.listView); recyclerView.setLayoutManager(new LinearLayoutManager(this)); + DividerItemDecoration itemDecor = new DividerItemDecoration(recyclerView.getContext(), DividerItemDecoration.VERTICAL); + recyclerView.addItemDecoration(itemDecor); addNotifications(); } @@ -58,11 +61,10 @@ 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"); - }); + }, throwable -> Timber.e(throwable, "Error occurred while loading notifications")); } private void handleUrl(String url) { diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java index 9bf3cec93..a5aac0508 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java @@ -8,13 +8,9 @@ import android.widget.TextView; import com.pedrogomez.renderers.Renderer; -import java.util.Calendar; -import java.util.Date; - import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.utils.DateUtils; /** * Created by root on 19.12.2017. diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 7dfba7a64..66be88e3f 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -25,7 +25,6 @@ import java.io.File; import javax.inject.Inject; import javax.inject.Named; -import dagger.android.AndroidInjection; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index 99c9f253b..fa0e43957 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -5,6 +5,7 @@ import android.accounts.AccountManager; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; +import android.net.Uri; import android.support.annotation.NonNull; import android.support.design.widget.NavigationView; import android.support.v4.widget.DrawerLayout; @@ -119,8 +120,9 @@ public abstract class NavigationBaseActivity extends BaseActivity return true; case R.id.action_feedback: drawerLayout.closeDrawer(navigationView); - Intent feedbackIntent = new Intent(Intent.ACTION_SEND); + Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO); feedbackIntent.setType("message/rfc822"); + feedbackIntent.setData(Uri.parse("mailto:")); feedbackIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{CommonsApplication.FEEDBACK_EMAIL}); feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/CompatTextView.java b/app/src/main/java/fr/free/nrw/commons/ui/widget/CompatTextView.java index a04f7f8c8..abe2e2554 100644 --- a/app/src/main/java/fr/free/nrw/commons/ui/widget/CompatTextView.java +++ b/app/src/main/java/fr/free/nrw/commons/ui/widget/CompatTextView.java @@ -1,7 +1,7 @@ package fr.free.nrw.commons.ui.widget; -/** - * Created by mikel on 07/08/2017. +/* + *Created by mikel on 07/08/2017. */ import android.content.Context; @@ -20,20 +20,22 @@ import fr.free.nrw.commons.utils.UiUtils; * a text view compatible with older versions of the platform */ public class CompatTextView extends AppCompatTextView { - + /** * Constructs a new instance of CompatTextView + * * @param context the view context */ public CompatTextView(Context context) { super(context); init(null); } - + /** * Constructs a new instance of CompatTextView + * * @param context the view context - * @param attrs the set of attributes for the view + * @param attrs the set of attributes for the view */ public CompatTextView(Context context, AttributeSet attrs) { super(context, attrs); @@ -42,6 +44,7 @@ public class CompatTextView extends AppCompatTextView { /** * Constructs a new instance of CompatTextView + * * @param context * @param attrs * @param defStyleAttr @@ -53,6 +56,7 @@ public class CompatTextView extends AppCompatTextView { /** * initializes the view + * * @param attrs the attribute set of the view, which can be null */ private void init(@Nullable AttributeSet attrs) { diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java b/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java index bf7742843..58fccf4d7 100644 --- a/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java +++ b/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java @@ -19,7 +19,7 @@ public abstract class OverlayDialog extends DialogFragment { /** * creates a DialogFragment with the correct style and theme - * @param savedInstanceState + * @param savedInstanceState bundle re-constructed from a previous saved state */ @Override public void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java new file mode 100644 index 000000000..b383601ec --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java @@ -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. + * + *

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). + * + *

todo: Detect selfies? + */ + +public class DetectUnwantedPicturesAsync extends AsyncTask { + + 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); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index 0026a6ce0..13056ad4b 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -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; @@ -28,7 +33,7 @@ public class FileUtils { * other file-based ContentProviders. * * @param context The context. - * @param uri The Uri to query. + * @param uri The Uri to query. * @author paulburke */ // Can be safely suppressed, checks for isKitKat before running isDocumentUri @@ -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,19 +61,26 @@ 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]; Uri contentUri = null; - if ("image".equals(type)) { - contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; - } else if ("video".equals(type)) { - contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; - } else if ("audio".equals(type)) { - contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + switch (type) { + case "image": + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + break; + case "video": + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + break; + case "audio": + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + break; + default: + break; } final String selection = "_id=?"; @@ -75,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; @@ -105,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 }; @@ -159,7 +211,8 @@ public class FileUtils { /** * Copy content from source file to destination file. - * @param source stream copied from + * + * @param source stream copied from * @param destination stream copied to * @throws IOException thrown when failing to read source or opening destination file */ @@ -172,7 +225,8 @@ public class FileUtils { /** * Copy content from source file to destination file. - * @param source file descriptor copied from + * + * @param source file descriptor copied from * @param destination file path copied to * @throws IOException thrown when failing to read source or opening destination file */ diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java index 404177032..b9750e350 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java @@ -113,11 +113,11 @@ public class GPSExtractor { */ @Nullable public String getCoords(boolean useGPS) { - String latitude = ""; - String longitude = ""; - String latitude_ref = ""; - String longitude_ref = ""; - String decimalCoords = ""; + String latitude; + String longitude; + String latitudeRef; + String longitudeRef; + String decimalCoords; //If image has no EXIF data and user has enabled GPS setting, get user's location if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) { @@ -150,15 +150,15 @@ public class GPSExtractor { Timber.d("EXIF data has location info"); latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE); - latitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF); + latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF); longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE); - longitude_ref = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF); + longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF); - if (latitude!=null && latitude_ref!=null && longitude!=null && longitude_ref!=null) { - Timber.d("Latitude: %s %s", latitude, latitude_ref); - Timber.d("Longitude: %s %s", longitude, longitude_ref); + if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) { + Timber.d("Latitude: %s %s", latitude, latitudeRef); + Timber.d("Longitude: %s %s", longitude, longitudeRef); - decimalCoords = getDecimalCoords(latitude, latitude_ref, longitude, longitude_ref); + decimalCoords = getDecimalCoords(latitude, latitudeRef, longitude, longitudeRef); return decimalCoords; } else { return null; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java index a41938cf7..ac0afa979 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java @@ -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; @@ -51,11 +54,17 @@ public class MultipleShareActivity extends AuthenticatedActivity MultipleUploadListFragment.OnMultipleUploadInitiatedHandler, OnCategoriesSaveHandler { - @Inject MediaWikiApi mwApi; - @Inject SessionManager sessionManager; - @Inject UploadController uploadController; - @Inject ModifierSequenceDao modifierSequenceDao; - @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject + MediaWikiApi mwApi; + @Inject + SessionManager sessionManager; + @Inject + UploadController uploadController; + @Inject + ModifierSequenceDao modifierSequenceDao; + @Inject + @Named("default_preferences") + SharedPreferences prefs; private ArrayList photosList = null; @@ -63,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); @@ -207,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 @@ -240,7 +259,7 @@ public class MultipleShareActivity extends AuthenticatedActivity mwApi.setAuthCookie(authCookie); Intent intent = getIntent(); - if (intent.getAction().equals(Intent.ACTION_SEND_MULTIPLE)) { + if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) { if (photosList == null) { photosList = new ArrayList<>(); ArrayList urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); @@ -252,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); } } @@ -278,7 +302,49 @@ public class MultipleShareActivity extends AuthenticatedActivity @Override public void onBackStackChanged() { - getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible()) ; + 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; + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java index 0b6e527e5..5b39b92f7 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java @@ -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); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java index 2f0a4977e..a5756c06b 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java @@ -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; @@ -16,7 +17,9 @@ import android.support.annotation.RequiresApi; import android.support.design.widget.Snackbar; 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; @@ -46,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; @@ -61,10 +67,10 @@ import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE; * Activity for the title/desc screen after image is selected. Also starts processing image * GPS coordinates or user location (if enabled in Settings) for category suggestions. */ -public class ShareActivity - extends AuthenticatedActivity +public class ShareActivity + extends AuthenticatedActivity implements SingleUploadFragment.OnUploadActionInitiated, - OnCategoriesSaveHandler { + OnCategoriesSaveHandler,SimilarImageDialogFragment.onResponse { private static final int REQUEST_PERM_ON_CREATE_STORAGE = 1; private static final int REQUEST_PERM_ON_CREATE_LOCATION = 2; @@ -72,12 +78,19 @@ public class ShareActivity private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4; private CategorizationFragment categorizationFragment; - @Inject MediaWikiApi mwApi; - @Inject CacheController cacheController; - @Inject SessionManager sessionManager; - @Inject UploadController uploadController; - @Inject ModifierSequenceDao modifierSequenceDao; - @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject + MediaWikiApi mwApi; + @Inject + CacheController cacheController; + @Inject + SessionManager sessionManager; + @Inject + UploadController uploadController; + @Inject + ModifierSequenceDao modifierSequenceDao; + @Inject + @Named("default_preferences") + SharedPreferences prefs; private String source; private String mimeType; @@ -89,6 +102,7 @@ public class ShareActivity private boolean cacheFound; private GPSExtractor imageObj; + private GPSExtractor tempImageObj; private String decimalCoords; private boolean useNewPermissions = false; @@ -99,7 +113,7 @@ public class ShareActivity private String description; private Snackbar snackbar; private boolean duplicateCheckPassed = false; - + private boolean haveCheckedForOtherImages = false; /** * Called when user taps the submit button. */ @@ -216,7 +230,7 @@ public class ShareActivity //Receive intent from ContributionController.java when user selects picture to upload Intent intent = getIntent(); - if (intent.getAction().equals(Intent.ACTION_SEND)) { + if (Intent.ACTION_SEND.equals(intent.getAction())) { mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM); if (intent.hasExtra(UploadService.EXTRA_SOURCE)) { source = intent.getStringExtra(UploadService.EXTRA_SOURCE); @@ -278,7 +292,7 @@ public class ShareActivity REQUEST_PERM_ON_CREATE_LOCATION); } } - performPreuploadProcessingOfFile(); + performPreUploadProcessingOfFile(); SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView"); @@ -302,7 +316,7 @@ public class ShareActivity && grantResults[0] == PackageManager.PERMISSION_GRANTED) { backgroundImageView.setImageURI(mediaUri); storagePermitted = true; - performPreuploadProcessingOfFile(); + performPreUploadProcessingOfFile(); } return; } @@ -310,7 +324,7 @@ public class ShareActivity if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { locationPermitted = true; - performPreuploadProcessingOfFile(); + performPreUploadProcessingOfFile(); } return; } @@ -319,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; } @@ -335,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(); @@ -346,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 @@ -361,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: "); @@ -375,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) { @@ -452,13 +507,93 @@ public class ShareActivity if (imageObj != null) { // Gets image coords from exif data or user location decimalCoords = imageObj.getCoords(gpsEnabled); - useImageCoords(); + if(decimalCoords==null || !imageObj.imageCoordsExists){ +// Check if the location is from GPS or EXIF +// Find other photos taken around the same time which has gps coordinates + Timber.d("EXIF:false"); + Timber.d("EXIF call"+(imageObj==tempImageObj)); + if(!haveCheckedForOtherImages) + findOtherImages(gpsEnabled);// Do not do repeat the process + } + else { +// As the selected image has GPS data in EXIF go ahead with the same. + useImageCoords(); + } } } catch (FileNotFoundException e) { Timber.w("File not found: " + mediaUri, e); } } + private void findOtherImages(boolean gpsEnabled) { + Timber.d("filePath"+getPathOfMediaOrCopy()); + String filePath = getPathOfMediaOrCopy(); + long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created + File folder = new File(filePath.substring(0,filePath.lastIndexOf('/'))); + File[] files = folder.listFiles(); + Timber.d("folderTime Number:"+files.length); + + for(File file : files){ + if(file.lastModified()-timeOfCreation<=(120*1000) && file.lastModified()-timeOfCreation>=-(120*1000)){ + //Make sure the photos were taken within 20seconds + Timber.d("fild date:"+file.lastModified()+ " time of creation"+timeOfCreation); + tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos + ParcelFileDescriptor descriptor + = null; + try { + descriptor = getContentResolver().openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r"); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (descriptor != null) { + tempImageObj = new GPSExtractor(descriptor.getFileDescriptor(),this, prefs); + } + } else { + if (filePath != null) { + tempImageObj = new GPSExtractor(file.getAbsolutePath(), this, prefs); + } + } + + if(tempImageObj!=null){ + Timber.d("not null fild EXIF"+tempImageObj.imageCoordsExists +" coords"+tempImageObj.getCoords(gpsEnabled)); + if(tempImageObj.getCoords(gpsEnabled)!=null && tempImageObj.imageCoordsExists){ +// Current image has gps coordinates and it's not current gps locaiton + Timber.d("This fild has image coords:"+ file.getAbsolutePath()); +// Create a dialog fragment for the suggestion + FragmentManager fragmentManager = getSupportFragmentManager(); + SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment(); + Bundle args = new Bundle(); + args.putString("originalImagePath",filePath); + args.putString("possibleImagePath",file.getAbsolutePath()); + newFragment.setArguments(args); + newFragment.show(fragmentManager, "dialog"); + break; + } + + } + + } + } + haveCheckedForOtherImages = true; //Finished checking for other images + return; + } + + @Override + public void onPostiveResponse() { + imageObj = tempImageObj; + decimalCoords = imageObj.getCoords(false);// Not necessary to use gps as image already ha EXIF data + Timber.d("EXIF from tempImageObj"); + useImageCoords(); + } + + @Override + public void onNegativeResponse() { + Timber.d("EXIF from imageObj"); + useImageCoords(); + + } + /** * Initiates retrieval of image coordinates or user coordinates, and caching of coordinates. * Then initiates the calls to MediaWiki API through an instance of MwVolleyApi. @@ -466,6 +601,7 @@ public class ShareActivity public void useImageCoords() { if (decimalCoords != null) { Timber.d("Decimal coords of image: %s", decimalCoords); + Timber.d("is EXIF data present:"+imageObj.imageCoordsExists+" from findOther image:"+(imageObj==tempImageObj)); // Only set cache for this point if image has coords if (imageObj.imageCoordsExists) { @@ -489,7 +625,10 @@ public class ShareActivity Timber.d("Cache found, setting categoryList in MwVolleyApi to %s", displayCatList); MwVolleyApi.setGpsCat(displayCatList); } + }else{ + Timber.d("EXIF: no coords"); } + } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java new file mode 100644 index 000000000..a8f336927 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java @@ -0,0 +1,112 @@ +package fr.free.nrw.commons.upload; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.graphics.drawable.VectorDrawableCompat; +import android.support.v4.app.DialogFragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.Button; + +import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; +import com.facebook.drawee.view.SimpleDraweeView; +import com.facebook.imagepipeline.listener.RequestListener; +import com.facebook.imagepipeline.listener.RequestLoggingListener; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import fr.free.nrw.commons.R; + +/** + * Created by harisanker on 14/2/18. + */ + +public class SimilarImageDialogFragment extends DialogFragment { + SimpleDraweeView originalImage; + SimpleDraweeView possibleImage; + Button positiveButton; + Button negativeButton; + onResponse mOnResponse;//Implemented interface from shareActivity + Boolean gotResponse = false; + public SimilarImageDialogFragment() { + } + public interface onResponse{ + public void onPostiveResponse(); + public void onNegativeResponse(); + } + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_similar_image_dialog, container, false); + Set requestListeners = new HashSet<>(); + requestListeners.add(new RequestLoggingListener()); + + originalImage =(SimpleDraweeView) view.findViewById(R.id.orginalImage); + possibleImage =(SimpleDraweeView) view.findViewById(R.id.possibleImage); + positiveButton = (Button) view.findViewById(R.id.postive_button); + negativeButton = (Button) view.findViewById(R.id.negative_button); + + originalImage.setHierarchy(GenericDraweeHierarchyBuilder + .newInstance(getResources()) + .setPlaceholderImage(VectorDrawableCompat.create(getResources(), + R.drawable.ic_image_black_24dp,getContext().getTheme())) + .setFailureImage(VectorDrawableCompat.create(getResources(), + R.drawable.ic_error_outline_black_24dp, getContext().getTheme())) + .build()); + possibleImage.setHierarchy(GenericDraweeHierarchyBuilder + .newInstance(getResources()) + .setPlaceholderImage(VectorDrawableCompat.create(getResources(), + R.drawable.ic_image_black_24dp,getContext().getTheme())) + .setFailureImage(VectorDrawableCompat.create(getResources(), + R.drawable.ic_error_outline_black_24dp, getContext().getTheme())) + .build()); + + originalImage.setImageURI(Uri.fromFile(new File(getArguments().getString("originalImagePath")))); + possibleImage.setImageURI(Uri.fromFile(new File(getArguments().getString("possibleImagePath")))); + + negativeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mOnResponse.onNegativeResponse(); + gotResponse = true; + dismiss(); + } + }); + positiveButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + mOnResponse.onPostiveResponse(); + gotResponse = true; + dismiss(); + } + }); + return view; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mOnResponse = (onResponse) getActivity();//Interface Implementation + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + return dialog; + } + + @Override + public void onDismiss(DialogInterface dialog) { +// I user dismisses dialog by pressing outside the dialog. + if(!gotResponse) + mOnResponse.onNegativeResponse(); + super.onDismiss(dialog); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index ade22ece6..32554da0f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -6,6 +6,7 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; +import android.content.res.AssetFileDescriptor; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; @@ -49,7 +50,7 @@ public class UploadController { private ServiceConnection uploadServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder binder) { - uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder)binder).getService(); + uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder).getService(); isUploadServiceConnected = true; } @@ -81,13 +82,14 @@ public class UploadController { /** * Starts a new upload task. - * @param title the title of the contribution - * @param mediaUri the media URI of the contribution - * @param description the description of the contribution - * @param mimeType the MIME type of the contribution - * @param source the source of the contribution + * + * @param title the title of the contribution + * @param mediaUri the media URI of the contribution + * @param description the description of the contribution + * @param mimeType the MIME type of the contribution + * @param source the source of the contribution * @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615") - * @param onComplete the progress tracker + * @param onComplete the progress tracker */ public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, ContributionUploadProgress onComplete) { Contribution contribution; @@ -106,8 +108,9 @@ public class UploadController { /** * Starts a new upload task. + * * @param contribution the contribution object - * @param onComplete the progress tracker + * @param onComplete the progress tracker */ public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) { //Set creator, desc, and license @@ -134,15 +137,17 @@ public class UploadController { ContentResolver contentResolver = context.getContentResolver(); try { if (contribution.getDataLength() <= 0) { - length = contentResolver - .openAssetFileDescriptor(contribution.getLocalUri(), "r") - .getLength(); - if (length == -1) { - // Let us find out the long way! - length = countBytes(contentResolver - .openInputStream(contribution.getLocalUri())); + AssetFileDescriptor assetFileDescriptor = contentResolver + .openAssetFileDescriptor(contribution.getLocalUri(), "r"); + if (assetFileDescriptor != null) { + length = assetFileDescriptor.getLength(); + if (length == -1) { + // Let us find out the long way! + length = countBytes(contentResolver + .openInputStream(contribution.getLocalUri())); + } + contribution.setDataLength(length); } - contribution.setDataLength(length); } } catch (IOException e) { Timber.e(e, "IO Exception: "); @@ -152,7 +157,7 @@ public class UploadController { Timber.e(e, "Security Exception: "); } - String mimeType = (String)contribution.getTag("mimeType"); + String mimeType = (String) contribution.getTag("mimeType"); Boolean imagePrefix = false; if (mimeType == null || TextUtils.isEmpty(mimeType) || mimeType.endsWith("*")) { @@ -199,6 +204,7 @@ public class UploadController { /** * Counts the number of bytes in {@code stream}. + * * @param stream the stream * @return the number of bytes in {@code stream} * @throws IOException if an I/O error occurs diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 1c8f9ac58..94c005256 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -161,7 +161,7 @@ public class UploadService extends HandlerService { @Override public int onStartCommand(Intent intent, int flags, int startId) { - if (intent.getAction().equals(ACTION_START_SERVICE) && freshStart) { + if (ACTION_START_SERVICE.equals(intent.getAction()) && freshStart) { ContentValues failedValues = new ContentValues(); failedValues.put(ContributionDao.Table.COLUMN_STATE, Contribution.STATE_FAILED); diff --git a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java index 6912d0ceb..d56a7b608 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java @@ -11,7 +11,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; -import fr.free.nrw.commons.CommonsApplication; import timber.log.Timber; public class FileUtils { @@ -32,7 +31,7 @@ public class FileUtils { reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); String line; while ((line = reader.readLine()) != null) { - buffer.append(line + "\n"); + buffer.append(line).append("\n"); } } finally { if (reader != null) { diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java new file mode 100644 index 000000000..6627f2886 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java @@ -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. + * + *

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; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java index 947c70eb6..f2a02398f 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java @@ -7,11 +7,6 @@ import android.widget.Toast; public class ViewUtil { public static void showLongToast(final Context context, @StringRes final int stringResId) { - ExecutorUtils.uiExecutor().execute(new Runnable() { - @Override - public void run() { - Toast.makeText(context, context.getString(stringResId), Toast.LENGTH_LONG).show(); - } - }); + ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResId), Toast.LENGTH_LONG).show()); } } diff --git a/app/src/main/res/layout-land/activity_login.xml b/app/src/main/res/layout-land/activity_login.xml index 9ecaf9855..a87084f66 100644 --- a/app/src/main/res/layout-land/activity_login.xml +++ b/app/src/main/res/layout-land/activity_login.xml @@ -13,12 +13,12 @@ @@ -40,11 +40,26 @@ android:textColor="@android:color/white" android:textSize="@dimen/heading_text_size" /> + + @@ -56,6 +71,7 @@ android:layout_marginLeft="@dimen/standard_gap" android:layout_marginRight="@dimen/standard_gap" android:layout_marginStart="@dimen/standard_gap" + android:gravity="center" android:paddingBottom="@dimen/small_gap" android:paddingTop="@dimen/small_gap" android:textColor="@color/secondaryDarkColor" @@ -149,8 +165,8 @@