diff --git a/app/build.gradle b/app/build.gradle index f43fa2820..ec9ab5643 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,20 +12,26 @@ dependencies { compile 'ch.acra:acra:4.7.0' compile 'org.mediawiki:api:1.3' compile 'commons-codec:commons-codec:1.10' - compile "com.android.support:support-v4:${project.supportLibVersion}" - compile "com.android.support:appcompat-v7:${project.supportLibVersion}" - compile "com.android.support:design:${project.supportLibVersion}" - compile 'com.google.code.gson:gson:2.8.0' - compile "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" compile 'com.github.pedrovgs:renderers:3.3.3' - annotationProcessor "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" + compile 'com.google.code.gson:gson:2.8.1' compile 'com.jakewharton.timber:timber:4.5.1' - compile 'com.squareup.okhttp3:okhttp:3.8.1' - compile 'com.squareup.okio:okio:1.13.0' + compile 'info.debatty:java-string-similarity:0.24' compile ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.1.0@aar'){ transitive=true } + compile "com.android.support:support-v4:${project.supportLibVersion}" + compile "com.android.support:appcompat-v7:${project.supportLibVersion}" + compile "com.android.support:design:${project.supportLibVersion}" + + compile "com.android.support:cardview-v7:${project.supportLibVersion}" + + compile "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" + annotationProcessor "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" + + compile 'com.squareup.okhttp3:okhttp:3.8.1' + compile 'com.squareup.okio:okio:1.13.0' + compile '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. @@ -39,12 +45,12 @@ dependencies { compile 'com.facebook.stetho:stetho:1.5.0' testCompile 'junit:junit:4.12' - testCompile 'org.robolectric:robolectric:3.3.2' + testCompile 'org.robolectric:robolectric:3.4' testCompile 'com.squareup.okhttp3:mockwebserver:3.8.1' androidTestCompile 'com.squareup.okhttp3:mockwebserver:3.8.1' androidTestCompile "com.android.support:support-annotations:${project.supportLibVersion}" - androidTestCompile ('com.android.support.test.espresso:espresso-core:2.2.2'){ + androidTestCompile ('com.android.support.test.espresso:espresso-core:3.0.1'){ exclude group: 'com.google.code.findbugs' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ab69029bd..44577db4c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,8 +19,13 @@ android:name=".CommonsApplication" android:icon="@drawable/ic_launcher" android:label="@string/app_name" - android:supportsRtl="true" - android:theme="@style/Theme.AppCompat"> + android:theme="@style/LightAppTheme" + android:supportsRtl="true" > + applicationInjector() { + protected AndroidInjector applicationInjector() { return injector(); } @@ -120,8 +120,7 @@ public class CommonsApplication extends DaggerApplication { //TODO: fix preference manager defaultPrefs.edit().clear().commit(); applicationPrefs.edit().clear().commit(); - applicationPrefs.edit().putBoolean("firstrun", false).apply(); - otherPrefs.edit().clear().commit(); + applicationPrefs.edit().putBoolean("firstrun", false).apply();otherPrefs.edit().clear().commit(); updateAllDatabases(); logoutListener.onLogoutComplete(); diff --git a/app/src/main/java/fr/free/nrw/commons/License.java b/app/src/main/java/fr/free/nrw/commons/License.java index d1aea6269..797e0f50c 100644 --- a/app/src/main/java/fr/free/nrw/commons/License.java +++ b/app/src/main/java/fr/free/nrw/commons/License.java @@ -3,10 +3,10 @@ package fr.free.nrw.commons; import android.support.annotation.Nullable; public class License { - String key; - String template; - String url; - String name; + private String key; + private String template; + private String url; + private String name; public License(String key, String template, String url, String name) { if (key == null) { 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 faf016e38..88a3f929f 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -1,5 +1,8 @@ package fr.free.nrw.commons; +import android.content.Context; +import android.preference.PreferenceManager; + import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.digest.DigestUtils; @@ -9,9 +12,9 @@ import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; +import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.settings.Prefs; - public class Utils { /** @@ -76,10 +79,14 @@ public class Utils { extension = "jpg"; } title = jpegPattern.matcher(title).replaceFirst(".jpg"); - if (extension != null && !title.toLowerCase(Locale.getDefault()).endsWith("." + extension.toLowerCase(Locale.ENGLISH))) { + if (extension != null && !title.toLowerCase(Locale.getDefault()) + .endsWith("." + extension.toLowerCase(Locale.ENGLISH))) { title += "." + extension; } return title; } + public static boolean isDarkTheme(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("theme", false); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java index d24d00d0c..e793d5eb9 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java @@ -21,7 +21,7 @@ public abstract class AuthenticatedActivity extends NavigationBaseActivity { @Inject SessionManager sessionManager; private String authCookie; - + private void getAuthCookie(Account account, AccountManager accountManager) { Single.fromCallable(() -> accountManager.blockingGetAuthToken(account, "", false)) 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 1900fc91d..44dc4b3a7 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 @@ -5,15 +5,21 @@ import android.app.ProgressDialog; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; +import android.support.annotation.ColorRes; +import android.support.annotation.NonNull; +import android.support.annotation.StringRes; import android.support.v4.app.NavUtils; +import android.support.v4.content.ContextCompat; +import android.support.v7.app.AppCompatDelegate; import android.text.Editable; import android.text.TextWatcher; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; -import android.widget.Toast; import javax.inject.Inject; import javax.inject.Named; @@ -24,6 +30,7 @@ import dagger.android.AndroidInjection; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.PageTitle; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.mwapi.MediaWikiApi; @@ -32,7 +39,6 @@ import timber.log.Timber; import static android.view.KeyEvent.KEYCODE_ENTER; import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; - public class LoginActivity extends AccountAuthenticatorActivity { public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username"; @@ -40,7 +46,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { @Inject MediaWikiApi mwApi; @Inject AccountUtil accountUtil; @Inject SessionManager sessionManager; - @Inject @Named("application_preferences") SharedPreferences prefs = null; + @Inject @Named("application_preferences") SharedPreferences prefs; @Inject @Named("default_preferences") SharedPreferences defaultPrefs; @BindView(R.id.loginButton) Button loginButton; @@ -48,16 +54,22 @@ public class LoginActivity extends AccountAuthenticatorActivity { @BindView(R.id.loginUsername) EditText usernameEdit; @BindView(R.id.loginPassword) EditText passwordEdit; @BindView(R.id.loginTwoFactor) EditText twoFactorEdit; - + @BindView(R.id.error_message_container) ViewGroup errorMessageContainer; + @BindView(R.id.error_message) TextView errorMessage; ProgressDialog progressDialog; + private AppCompatDelegate delegate; private LoginTextWatcher textWatcher = new LoginTextWatcher(); @Override public void onCreate(Bundle savedInstanceState) { + setTheme(Utils.isDarkTheme(this) ? R.style.DarkAppTheme : R.style.LightAppTheme); + getDelegate().installViewFactory(); + getDelegate().onCreate(savedInstanceState); AndroidInjection.inject(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); + ButterKnife.bind(this); usernameEdit.addTextChangedListener(textWatcher); @@ -65,45 +77,17 @@ public class LoginActivity extends AccountAuthenticatorActivity { twoFactorEdit.addTextChangedListener(textWatcher); passwordEdit.setOnEditorActionListener(newLoginInputActionListener()); - loginButton.setOnClickListener(this::performLogin); - signupButton.setOnClickListener(this::signUp); + loginButton.setOnClickListener(view -> performLogin()); + signupButton.setOnClickListener(view -> signUp()); } - private class LoginTextWatcher implements TextWatcher { - @Override - public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int start, int count, int after) { - } - - @Override - public void afterTextChanged(Editable editable) { - if (usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0 && - (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE)) { - loginButton.setEnabled(true); - } else { - loginButton.setEnabled(false); - } - } - } - - private TextView.OnEditorActionListener newLoginInputActionListener() { - return (textView, actionId, keyEvent) -> { - if (loginButton.isEnabled()) { - if (actionId == IME_ACTION_DONE) { - performLogin(textView); - return true; - } else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) { - performLogin(textView); - return true; - } - } - return false; - }; + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + getDelegate().onPostCreate(savedInstanceState); } + @Override protected void onResume() { super.onResume(); if (prefs.getBoolean("firstrun", true)) { @@ -128,15 +112,10 @@ public class LoginActivity extends AccountAuthenticatorActivity { usernameEdit.removeTextChangedListener(textWatcher); passwordEdit.removeTextChangedListener(textWatcher); twoFactorEdit.removeTextChangedListener(textWatcher); + delegate.onDestroy(); super.onDestroy(); } - private void performLogin(View view) { - Timber.d("Login to start!"); - LoginTask task = getLoginTask(); - task.execute(); - } - private LoginTask getLoginTask() { return new LoginTask( this, @@ -156,6 +135,29 @@ public class LoginActivity extends AccountAuthenticatorActivity { return new PageTitle(username).getText(); } + @Override + protected void onStart() { + super.onStart(); + delegate.onStart(); + } + + @Override + protected void onStop() { + super.onStop(); + delegate.onStop(); + } + + @Override + protected void onPostResume() { + super.onPostResume(); + getDelegate().onPostResume(); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + getDelegate().setContentView(view, params); + } + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { @@ -166,36 +168,28 @@ public class LoginActivity extends AccountAuthenticatorActivity { return super.onOptionsItemSelected(item); } - /** - * Called when Sign Up button is clicked. - * @param view View - */ - public void signUp(View view) { - Intent intent = new Intent(this, SignupActivity.class); - startActivity(intent); + @Override + @NonNull + public MenuInflater getMenuInflater() { + return getDelegate().getMenuInflater(); } public void askUserForTwoFactorAuth() { if (BuildConfig.DEBUG) { twoFactorEdit.setVisibility(View.VISIBLE); - showUserToastAndCancelDialog(R.string.login_failed_2fa_needed); + showMessageAndCancelDialog(R.string.login_failed_2fa_needed); } else { - showUserToastAndCancelDialog(R.string.login_failed_2fa_not_supported); + showMessageAndCancelDialog(R.string.login_failed_2fa_not_supported); } } - public void showUserToastAndCancelDialog(int resId) { - showUserToast(resId); + public void showMessageAndCancelDialog(@StringRes int resId) { + showMessage(resId, R.color.secondaryDarkColor); progressDialog.cancel(); } - private void showUserToast(int resId) { - Toast.makeText(this, resId, Toast.LENGTH_LONG).show(); - } - - public void showSuccessToastAndDismissDialog() { - Toast successToast = Toast.makeText(this, R.string.login_success, Toast.LENGTH_SHORT); - successToast.show(); + public void showSuccessAndDismissDialog() { + showMessage(R.string.login_success, R.color.primaryDarkColor); progressDialog.dismiss(); } @@ -209,4 +203,59 @@ public class LoginActivity extends AccountAuthenticatorActivity { finish(); } + private void performLogin() { + Timber.d("Login to start!"); + LoginTask task = getLoginTask(); + task.execute(); + } + + private void signUp() { + Intent intent = new Intent(this, SignupActivity.class); + startActivity(intent); + } + + private TextView.OnEditorActionListener newLoginInputActionListener() { + return (textView, actionId, keyEvent) -> { + if (loginButton.isEnabled()) { + if (actionId == IME_ACTION_DONE) { + performLogin(); + return true; + } else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) { + performLogin(); + return true; + } + } + return false; + }; + } + + private void showMessage(@StringRes int resId, @ColorRes int colorResId) { + errorMessage.setText(getString(resId)); + errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); + errorMessageContainer.setVisibility(View.VISIBLE); + } + + private AppCompatDelegate getDelegate() { + if (delegate == null) { + delegate = AppCompatDelegate.create(this, null); + } + return delegate; + } + + private class LoginTextWatcher implements TextWatcher { + @Override + public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { + } + + @Override + public void onTextChanged(CharSequence charSequence, int start, int count, int after) { + } + + @Override + public void afterTextChanged(Editable editable) { + boolean enabled = usernameEdit.getText().length() != 0 && passwordEdit.getText().length() != 0 && + (BuildConfig.DEBUG || twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE); + loginButton.setEnabled(enabled); + } + } } 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 index a777187d0..fa7a671c7 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java @@ -84,7 +84,7 @@ class LoginTask extends AsyncTask { } private void handlePassResult() { - loginActivity.showSuccessToastAndDismissDialog(); + loginActivity.showSuccessAndDismissDialog(); AccountAuthenticatorResponse response = null; @@ -111,27 +111,27 @@ class LoginTask extends AsyncTask { private void handleOtherResults(String result) { if (result.equals("NetworkFailure")) { // Matches NetworkFailure which is created by the doInBackground method - loginActivity.showUserToastAndCancelDialog(R.string.login_failed_network); + loginActivity.showMessageAndCancelDialog(R.string.login_failed_network); } else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { // Matches nosuchuser, nosuchusershort, noname - loginActivity.showUserToastAndCancelDialog(R.string.login_failed_username); + loginActivity.showMessageAndCancelDialog(R.string.login_failed_username); loginActivity.emptySensitiveEditFields(); } else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) { // Matches wrongpassword, wrongpasswordempty - loginActivity.showUserToastAndCancelDialog(R.string.login_failed_password); + loginActivity.showMessageAndCancelDialog(R.string.login_failed_password); loginActivity.emptySensitiveEditFields(); } else if (result.toLowerCase().contains("throttle".toLowerCase())) { // Matches unknown throttle error codes - loginActivity.showUserToastAndCancelDialog(R.string.login_failed_throttled); + loginActivity.showMessageAndCancelDialog(R.string.login_failed_throttled); } else if (result.toLowerCase().contains("userblocked".toLowerCase())) { // Matches login-userblocked - loginActivity.showUserToastAndCancelDialog(R.string.login_failed_blocked); + 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.showUserToastAndCancelDialog(R.string.login_failed_generic); + loginActivity.showMessageAndCancelDialog(R.string.login_failed_generic); } } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java index 1ea15ff65..a6b66cbf6 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java @@ -24,7 +24,8 @@ public class SignupActivity extends BaseActivity { webView.setWebViewClient(new MyWebViewClient()); WebSettings webSettings = webView.getSettings(); - //Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can trust Wikimedia's site... right? + /*Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can + trust Wikimedia's site... right?*/ webSettings.setJavaScriptEnabled(true); webView.loadUrl(BuildConfig.SIGNUP_LANDING_URL); diff --git a/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java b/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java index 85f545567..ff6ceece4 100644 --- a/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java +++ b/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java @@ -32,7 +32,7 @@ public class CacheController { public void cacheCategory() { List pointCatList = new ArrayList<>(); if (MwVolleyApi.GpsCatExists.getGpsCatExists()) { - pointCatList.addAll(MwVolleyApi.getGpsCat()); + pointCatList.addAll(MwVolleyApi.getGpsCat()); Timber.d("Categories being cached: %s", pointCatList); } else { Timber.d("No categories found, so no categories cached"); 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 9883c3441..5c3992bf2 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 @@ -23,6 +23,7 @@ import com.pedrogomez.renderers.RVRendererAdapter; import java.util.ArrayList; import java.util.Calendar; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -38,6 +39,7 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.data.Category; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.upload.MwVolleyApi; +import fr.free.nrw.commons.utils.StringSortingUtils; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; @@ -112,7 +114,7 @@ public class CategorizationFragment extends DaggerFragment { RxTextView.textChanges(categoriesFilter) .takeUntil(RxView.detaches(categoriesFilter)) - .debounce(300, TimeUnit.MILLISECONDS) + .debounce(500, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe(filter -> updateCategoryList(filter.toString())); return rootView; @@ -200,11 +202,12 @@ public class CategorizationFragment extends DaggerFragment { .concatWith( searchAll(filter) .mergeWith(searchCategories(filter)) - .concatWith( TextUtils.isEmpty(filter) + .concatWith(TextUtils.isEmpty(filter) ? defaultCategories() : Observable.empty()) ) .filter(categoryItem -> !containsYear(categoryItem.getName())) .distinct() + .sorted(sortBySimilarity(filter)) .observeOn(AndroidSchedulers.mainThread()) .subscribe( s -> categoriesAdapter.add(s), @@ -228,6 +231,12 @@ public class CategorizationFragment extends DaggerFragment { ); } + private Comparator sortBySimilarity(final String filter) { + Comparator stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter); + return (firstItem, secondItem) -> stringSimilarityComparator + .compare(firstItem.getName(), secondItem.getName()); + } + private List getStringList(List input) { List output = new ArrayList<>(); for (CategoryItem item : input) { diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java index 9c336429c..ed698ec4c 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryContentProvider.java @@ -13,7 +13,6 @@ import android.text.TextUtils; import javax.inject.Inject; import dagger.android.AndroidInjection; -import fr.free.nrw.commons.data.Category; import fr.free.nrw.commons.data.DBOpenHelper; import timber.log.Timber; @@ -142,11 +141,13 @@ public class CategoryContentProvider extends ContentProvider { public int update(@NonNull Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) { /* - SQL Injection warnings: First, note that we're not exposing this to the outside world (exported="false") - Even then, we should make sure to sanitize all user input appropriately. Input that passes through ContentValues + SQL Injection warnings: First, note that we're not exposing this to the + outside world (exported="false"). Even then, we should make sure to sanitize + all user input appropriately. Input that passes through ContentValues should be fine. So only issues are those that pass in via concating. - In here, the only concat created argument is for id. It is cast to an int, and will error out otherwise. + In here, the only concat created argument is for id. It is cast to an int, + and will error out otherwise. */ int uriType = uriMatcher.match(uri); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); 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 cfc25b36f..5ec290026 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 @@ -156,10 +156,12 @@ public class ContributionsContentProvider extends ContentProvider { public int update(@NonNull Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) { /* SQL Injection warnings: First, note that we're not exposing this to the outside world (exported="false") - Even then, we should make sure to sanitize all user input appropriately. Input that passes through ContentValues - should be fine. So only issues are those that pass in via concating. + Even then, we should make sure to sanitize all user input appropriately. + Input that passes through ContentValuesshould be fine. So only issues are those that pass + in via concating. - In here, the only concat created argument is for id. It is cast to an int, and will error out otherwise. + In here, the only concat created argument is for id. It is cast to an int, and will + error out otherwise. */ int uriType = uriMatcher.match(uri); SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); diff --git a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java index f2accfd51..36012e55e 100644 --- a/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java +++ b/app/src/main/java/fr/free/nrw/commons/modifications/ModifierSequence.java @@ -27,7 +27,7 @@ public class ModifierSequence { public ModifierSequence(Uri mediaUri, JSONObject data) { this(mediaUri); JSONArray modifiersJSON = data.optJSONArray("modifiers"); - for(int i=0; i< modifiersJSON.length(); i++) { + for (int i=0; i< modifiersJSON.length(); i++) { modifiers.add(PageModifier.fromJSON(modifiersJSON.optJSONObject(i))); } } @@ -41,7 +41,7 @@ public class ModifierSequence { } public String executeModifications(String pageName, String pageContents) { - for(PageModifier modifier: modifiers) { + for (PageModifier modifier: modifiers) { pageContents = modifier.doModification(pageName, pageContents); } return pageContents; @@ -60,7 +60,7 @@ public class ModifierSequence { JSONObject data = new JSONObject(); try { JSONArray modifiersJSON = new JSONArray(); - for(PageModifier modifier: modifiers) { + for (PageModifier modifier: modifiers) { modifiersJSON.put(modifier.toJSON()); } data.put("modifiers", modifiersJSON); @@ -81,7 +81,8 @@ public class ModifierSequence { // Hardcoding column positions! ModifierSequence ms = null; try { - ms = new ModifierSequence(Uri.parse(cursor.getString(1)), new JSONObject(cursor.getString(2))); + ms = new ModifierSequence(Uri.parse(cursor.getString(1)), + new JSONObject(cursor.getString(2))); } catch (JSONException e) { throw new RuntimeException(e); } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 1e059f59b..ef99b7382 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -391,7 +391,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { public UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, final ProgressListener progressListener) throws IOException { ApiResult result = api.upload(filename, file, dataLength, pageContents, editSummary, progressListener::onProgress); - Log.e("WTF", "Result: "+result.toString()); + Log.e("WTF", "Result: " +result.toString()); String resultStatus = result.getString("/api/upload/@result"); if (!resultStatus.equals("Success")) { 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 eac0b46df..c8693a76f 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 @@ -10,6 +10,7 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; @@ -49,26 +50,45 @@ public class NearbyActivity extends NavigationBaseActivity { private boolean isMapViewActive = false; private static final int LOCATION_REQUEST = 1; + private static final String MAP_LAST_USED_PREFERENCE = "mapLastUsed"; private LocationServiceManager locationManager; private LatLng curLatLang; private Bundle bundle; private NearbyAsyncTask nearbyAsyncTask; + private SharedPreferences sharedPreferences; + private NearbyActivityMode viewMode; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); setContentView(R.layout.activity_nearby); ButterKnife.bind(this); checkLocationPermission(); bundle = new Bundle(); initDrawer(); + initViewState(); + } + + private void initViewState() { + if (sharedPreferences.getBoolean(MAP_LAST_USED_PREFERENCE, false)) { + viewMode = NearbyActivityMode.MAP; + } else { + viewMode = NearbyActivityMode.LIST; + } } @Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.menu_nearby, menu); + + if (viewMode.isMap()) { + MenuItem item = menu.findItem(R.id.action_toggle_view); + item.setIcon(viewMode.getIcon()); + } + return super.onCreateOptionsMenu(menu); } @@ -79,13 +99,10 @@ public class NearbyActivity extends NavigationBaseActivity { case R.id.action_refresh: refreshView(); return true; - case R.id.action_map: - showMapView(); - if (isMapViewActive) { - item.setIcon(R.drawable.ic_list_white_24dp); - } else { - item.setIcon(R.drawable.ic_map_white_24dp); - } + case R.id.action_toggle_view: + viewMode = viewMode.toggle(); + item.setIcon(viewMode.getIcon()); + toggleView(); return true; default: return super.onOptionsItemSelected(item); @@ -163,15 +180,30 @@ public class NearbyActivity extends NavigationBaseActivity { if (progressBar != null) { progressBar.setVisibility(View.GONE); } - FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); - Fragment noPermissionsFragment = new NoPermissionsFragment(); - fragmentTransaction.replace(R.id.container, noPermissionsFragment); - fragmentTransaction.commit(); + + showLocationPermissionDeniedErrorDialog(); } } } } + private void showLocationPermissionDeniedErrorDialog() { + new AlertDialog.Builder(this) + .setMessage(R.string.nearby_needs_permissions) + .setCancelable(false) + .setPositiveButton(R.string.give_permission, (dialog, which) -> { + //will ask for the location permission again + checkLocationPermission(); + }) + .setNegativeButton(R.string.cancel, (dialog, which) -> { + //dismiss dialog and finish activity + dialog.cancel(); + finish(); + }) + .create() + .show(); + } + private void checkGps() { LocationManager manager = (LocationManager) getSystemService(LOCATION_SERVICE); if (!manager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { @@ -203,20 +235,16 @@ public class NearbyActivity extends NavigationBaseActivity { } } - private void showMapView() { + private void toggleView() { if (nearbyAsyncTask != null) { - if (!isMapViewActive) { - isMapViewActive = true; - if (nearbyAsyncTask.getStatus() == AsyncTask.Status.FINISHED) { + if (nearbyAsyncTask.getStatus() == AsyncTask.Status.FINISHED) { + if (viewMode.isMap()) { setMapFragment(); - } - - } else { - isMapViewActive = false; - if (nearbyAsyncTask.getStatus() == AsyncTask.Status.FINISHED) { + } else { setListFragment(); } } + sharedPreferences.edit().putBoolean(MAP_LAST_USED_PREFERENCE, viewMode.isMap()).apply(); } } @@ -292,7 +320,7 @@ public class NearbyActivity extends NavigationBaseActivity { bundle.putString("CurLatLng", gsonCurLatLng); // Begin the transaction - if (isMapViewActive) { + if (viewMode.isMap()) { setMapFragment(); } else { setListFragment(); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivityMode.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivityMode.java new file mode 100644 index 000000000..e46f53f66 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivityMode.java @@ -0,0 +1,30 @@ +package fr.free.nrw.commons.nearby; + +import android.support.annotation.DrawableRes; + +import fr.free.nrw.commons.R; + +enum NearbyActivityMode { + MAP(R.drawable.ic_list_white_24dp), + LIST(R.drawable.ic_map_white_24dp); + + @DrawableRes + private final int icon; + + NearbyActivityMode(int icon) { + this.icon = icon; + } + + @DrawableRes + public int getIcon() { + return icon; + } + + public NearbyActivityMode toggle() { + return isMap() ? LIST : MAP; + } + + public boolean isMap() { + return MAP.equals(this); + } +} \ No newline at end of file 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 1c8e9012e..184778fcc 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 @@ -309,7 +309,7 @@ public class MultipleShareActivity extends AuthenticatedActivity @Override public void onBackStackChanged() { - getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible()); + getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible()) ; } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java index 8038bd791..259f38c36 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java @@ -90,7 +90,8 @@ public class SingleUploadFragment extends DaggerFragment { } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_single_upload, container, false); ButterKnife.bind(this, rootView); diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java new file mode 100644 index 000000000..03b1469e0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java @@ -0,0 +1,46 @@ +package fr.free.nrw.commons.utils; + +import java.util.Comparator; + +import info.debatty.java.stringsimilarity.Levenshtein; + +public class StringSortingUtils { + + private StringSortingUtils() { + //no-op + } + + /** + * Returns Comparator for sorting strings by its similarity with Levenshtein + * algorithm. By using this Comparator we get results from the highest to + * the lowest match. + * + * @param filter pattern to compare similarity + * @return Comparator with string similarity + */ + + public static Comparator sortBySimilarity(final String filter) { + return (firstItem, secondItem) -> { + double firstItemSimilarity = calculateSimilarity(firstItem, filter); + double secondItemSimilarity = calculateSimilarity(secondItem, filter); + return (int) Math.signum(secondItemSimilarity - firstItemSimilarity); + }; + } + + private static double calculateSimilarity(String firstString, String secondString) { + String longer = firstString.toLowerCase(); + String shorter = secondString.toLowerCase(); + + if (firstString.length() < secondString.length()) { + longer = secondString; + shorter = firstString; + } + int longerLength = longer.length(); + if (longerLength == 0) { + return 1.0; + } + + double distanceBetweenStrings = new Levenshtein().distance(longer, shorter); + return (longerLength - distanceBetweenStrings) / (double) longerLength; + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/blue_rinse_circle.xml b/app/src/main/res/drawable/blue_rinse_circle.xml new file mode 100644 index 000000000..e63317a5b --- /dev/null +++ b/app/src/main/res/drawable/blue_rinse_circle.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_login.xml b/app/src/main/res/layout-land/activity_login.xml new file mode 100644 index 000000000..d96106f18 --- /dev/null +++ b/app/src/main/res/layout-land/activity_login.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +