diff --git a/.travis.yml b/.travis.yml
index 20c5bfaee..5e76e09d9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -19,12 +19,13 @@ android:
components:
- tools
- platform-tools
- - build-tools-26.0.2
+ - build-tools-27.0.0
- extra-google-m2repository
- extra-android-m2repository
- ${ANDROID_TARGET}
- android-25
- android-26
+ - android-27
- sys-img-${ANDROID_ABI}-${ANDROID_TARGET}
licenses:
- 'android-sdk-license-.+'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7f3dfc30a..1688b3b02 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Wikimedia Commons for Android
+## v2.7.2
+- Modified subtext for "automatically get current location" setting to emphasize that it will reveal user's location
+
+## v2.7.1
+- Fixed UI and permission issues with Nearby
+- Fixed issue with My Recent Uploads being empty
+- Fixed blank category issue when uploading directly from Nearby
+- Various crash fixes
+
## v2.7.0
- New Nearby Places UI with direct uploads (and associated category suggestions)
- Added two-factor authentication login
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ee7f42e06..caa02a103 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1 +1,34 @@
-Please see our guidelines in the wiki: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21
+Thanks for considering to contribute to this project! A few guidelines for
+people who want to contribute their code to this software are documented in
+[this project's Wiki](https://github.com/commons-app/apps-android-commons/wiki/Contributing-Guidelines).
+If you're not sure where to start head on to [this wiki page](https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome!).
+
+Here's a gist of the guidelines,
+
+1. Make separate commits for logically separate changes
+
+1. Describe your changes well in the commit message
+
+ The first line of the commit message should be a short description of what has
+changed. It is also good to prefix the first line with "area: " where the "area"
+is a filename or identifier for the general area of the code being modified.
+The body should provide a meaningful commit message.
+
+1. Write Javadocs
+
+ We require contributors to include Javadocs for all new methods and classes
+ submitted via PRs (after 1 May 2018). This is aimed at making it easier for
+ new contributors to dive into our codebase, especially those who are new to
+ Android development. A few things to note:
+
+ - This should not replace the need for code that is easily-readable in
+ and of itself
+ - Please make sure that your Javadocs are reasonably descriptive, not just
+ a copy of the method name
+ - Please do not use `@author` tags - we aim for collective code ownership,
+ and if needed, Git allows us to see who wrote something without needing
+ to add these tags (`git blame`)
+
+1. Write tests for your code (if possible)
+
+1. Make sure the Wiki pages don't become stale by updating them (if needed)
diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
index 34078f07e..37e104d14 100644
--- a/PULL_REQUEST_TEMPLATE.md
+++ b/PULL_REQUEST_TEMPLATE.md
@@ -1,15 +1,19 @@
-## Description
+## Title (required)
-Fixes #{GitHub issue number}
+Fixes #{GitHub issue number and title (Please do not forget adding title) }
+
+## Description (required)
+
+Fixes #{GitHub issue number and title}
{Describe the changes made and why they were made.}
-## Tests performed
+## Tests performed (required)
Tested on {API level & name of device/emulator}, with {build variant, e.g. ProdDebug}.
-{Please test your PR at least once before submitting.}
-
-## Screenshots showing what changed
+## Screenshots showing what changed (optional)
{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)}
+
+_Note: Please ensure that you have read CONTRIBUTING.md if this is your first pull request._
\ No newline at end of file
diff --git a/app/build.gradle b/app/build.gradle
index b1ee9aef9..68a31f984 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -7,10 +7,12 @@ apply from: 'quality.gradle'
apply plugin: 'com.getkeepsafe.dexcount'
dependencies {
+ implementation 'com.squareup.picasso:picasso:2.71828'
+ implementation 'com.prof.rssparser:rssparser:1.1'
implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07'
implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar'
implementation 'in.yuvi:http.fluent:1.3'
- implementation 'com.android.volley:volley:1.0.0'
+ implementation 'com.github.chrisbanes:PhotoView:2.0.0'
implementation 'ch.acra:acra:4.9.2'
implementation 'org.mediawiki:api:1.3'
implementation 'commons-codec:commons-codec:1.10'
@@ -18,69 +20,59 @@ 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.4.1@aar'){
- transitive=true
+ implementation 'com.borjabravo:readmoretextview:2.1.0'
+ implementation 'com.android.support.constraint:constraint-layout:1.0.2'
+ implementation('com.mapbox.mapboxsdk:mapbox-android-sdk:5.4.1@aar') {
+ transitive = true
}
-
-
+ implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
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"
-
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
-
- implementation 'com.squareup.okhttp3:okhttp:3.8.1'
+ implementation 'com.squareup.okhttp3:okhttp:3.9.1'
implementation 'com.squareup.okio:okio:1.13.0'
-
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.
implementation 'com.android.support:multidex:1.0.3'
-
- testImplementation "org.robolectric:multidex:3.4.2"
-
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'
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
-
+ implementation 'org.jsoup:jsoup:1.11.3'
implementation 'com.facebook.fresco:fresco:1.5.0'
implementation 'com.facebook.stetho:stetho:1.5.0'
-
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
-
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
-
+ testImplementation 'org.robolectric:multidex:3.4.2'
testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
- androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
-
+ testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
testImplementation 'junit:junit:4.12'
testImplementation 'org.robolectric:robolectric:3.7.1'
- testImplementation 'org.mockito:mockito-all:1.10.19'
-
+ testImplementation 'com.nhaarman:mockito-kotlin:1.5.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
+
+ implementation 'com.caverock:androidsvg:1.2.1'
+ implementation 'com.github.bumptech.glide:glide:4.7.1'
+ kapt 'com.github.bumptech.glide:compiler:4.7.1'
+
+ androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION"
- androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
+ androidTestImplementation 'com.android.support.test:rules:1.0.2'
+ androidTestImplementation 'com.android.support.test:runner:1.0.2'
+ androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY"
releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
testImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY"
-
- implementation "com.google.dagger:dagger:$DAGGER_VERSION"
- implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
- kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
- kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
-
- implementation 'com.borjabravo:readmoretextview:2.1.0'
- implementation 'com.android.support.constraint:constraint-layout:1.0.2'
}
android {
@@ -91,8 +83,8 @@ android {
defaultConfig {
applicationId 'fr.free.nrw.commons'
- versionCode 83
- versionName '2.7.0'
+ versionCode 85
+ versionName '2.7.2'
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
minSdkVersion project.minSdkVersion
@@ -121,7 +113,7 @@ android {
buildTypes {
release {
minifyEnabled false // See https://stackoverflow.com/questions/40232404/google-play-apk-and-android-studio-apk-usb-debug-behaving-differently - proguard.cfg modification alone insufficient.
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt', 'proguard-glide.txt'
}
debug {
applicationIdSuffix ".debug"
@@ -133,7 +125,9 @@ android {
flavorDimensions 'tier'
productFlavors {
prod {
+ buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
+ buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\""
@@ -149,7 +143,9 @@ android {
beta {
// What values do we need to hit the BETA versions of the site / api ?
+ buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\""
+ buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\""
diff --git a/app/libs/java-json.jar b/app/libs/java-json.jar
new file mode 100644
index 000000000..2f211e366
Binary files /dev/null and b/app/libs/java-json.jar differ
diff --git a/app/proguard-glide.txt b/app/proguard-glide.txt
new file mode 100644
index 000000000..ef3437660
--- /dev/null
+++ b/app/proguard-glide.txt
@@ -0,0 +1,9 @@
+-keep public class * implements com.bumptech.glide.module.GlideModule
+-keep public class * extends com.bumptech.glide.module.AppGlideModule
+-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
+ **[] $VALUES;
+ public *;
+}
+
+# for DexGuard only
+-keepresourcexmlelements manifest/application/meta-data@value=GlideModule
\ No newline at end of file
diff --git a/app/proguard-rules.txt b/app/proguard-rules.txt
index bbf3a3f0d..39b618718 100644
--- a/app/proguard-rules.txt
+++ b/app/proguard-rules.txt
@@ -1,5 +1,4 @@
-dontobfuscate
-keep class org.apache.http.** { *; }
-dontwarn org.apache.http.**
--keep class fr.free.nrw.commons.upload.MwVolleyApi$Page {*;}
-keep class android.support.v7.widget.ShareActionProvider { *; }
\ No newline at end of file
diff --git a/app/quality.gradle b/app/quality.gradle
index 7ea20916a..1afdf0d68 100644
--- a/app/quality.gradle
+++ b/app/quality.gradle
@@ -18,7 +18,7 @@ task checkstyle(type: Checkstyle) {
reports {
html {
enabled true
- destination "${project.buildDir}/reports/checkstyle/checkstyle.html"
+ destination file("${project.buildDir}/reports/checkstyle/checkstyle.html")
}
}
}
@@ -36,10 +36,10 @@ task pmd(type: Pmd) {
xml.enabled = false
html.enabled = true
xml {
- destination "${project.buildDir}/reports/pmd/pmd.xml"
+ destination file("${project.buildDir}/reports/pmd/pmd.xml")
}
html {
- destination "${project.buildDir}/reports/pmd/pmd.html"
+ destination file("${project.buildDir}/reports/pmd/pmd.html")
}
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 253bdaea8..38fc2e35a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -15,6 +15,7 @@
+
@@ -26,10 +27,10 @@
android:theme="@style/LightAppTheme"
android:supportsRtl="true" >
+ android:theme="@android:style/Theme.Dialog"
+ android:launchMode="singleInstance"
+ android:excludeFromRecents="true"
+ android:finishOnTaskLaunch="true" />
@@ -91,6 +92,11 @@
android:name=".notification.NotificationActivity"
android:label="@string/navigation_item_notification" />
+
+
+
+
+
+
+
+
+
+
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 e6bf34736..c8941dcd8 100644
--- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java
@@ -9,9 +9,6 @@ import android.os.Bundle;
import android.text.Html;
import android.text.SpannableString;
import android.text.style.UnderlineSpan;
-import android.util.Log;
-import android.support.customtabs.CustomTabsIntent;
-import android.support.v4.content.ContextCompat;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -20,7 +17,6 @@ import android.widget.ArrayAdapter;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
-import android.widget.Toast;
import butterknife.BindView;
import butterknife.ButterKnife;
@@ -28,8 +24,6 @@ import butterknife.OnClick;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.ui.widget.HtmlTextView;
-import static android.widget.Toast.LENGTH_SHORT;
-
/**
* Represents about screen of this app
*/
@@ -135,9 +129,10 @@ public class AboutActivity extends NavigationBaseActivity {
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.share_app_icon:
+ String shareText = "Upload photos to Wikimedia Commons on your phone\nDownload the Commons app: http://play.google.com/store/apps/details?id=fr.free.nrw.commons";
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
- sendIntent.putExtra(Intent.EXTRA_TEXT, "http://play.google.com/store/apps/details?id=fr.free.nrw.commons");
+ sendIntent.putExtra(Intent.EXTRA_TEXT, shareText);
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, "Share app via..."));
return true;
diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java
index 57cb5fad1..61eecee00 100644
--- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java
+++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java
@@ -1,6 +1,5 @@
package fr.free.nrw.commons;
-import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase;
@@ -27,7 +26,7 @@ import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
-import fr.free.nrw.commons.utils.FileUtils;
+import fr.free.nrw.commons.upload.FileUtils;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
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 2d79a6c4f..affb57528 100644
--- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java
+++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java
@@ -61,8 +61,8 @@ public class MediaDataExtractor {
}
try{
- Timber.d("Nominated for deletion: " + mediaWikiApi.pageExists("Commons:Deletion_requests/"+filename));
- deletionStatus = mediaWikiApi.pageExists("Commons:Deletion_requests/"+filename);
+ deletionStatus = mediaWikiApi.pageExists("Commons:Deletion_requests/" + filename);
+ Timber.d("Nominated for deletion: " + deletionStatus);
}
catch (Exception e){
Timber.d(e.getMessage());
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 91c23ce26..9f89586c0 100644
--- a/app/src/main/java/fr/free/nrw/commons/Utils.java
+++ b/app/src/main/java/fr/free/nrw/commons/Utils.java
@@ -178,6 +178,7 @@ public class Utils {
}
public static void handleWebUrl(Context context, Uri url) {
+ Timber.d("Launching web url %s", url.toString());
Intent browserIntent = new Intent(Intent.ACTION_VIEW, url);
if (browserIntent.resolveActivity(context.getPackageManager()) == null) {
Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT);
diff --git a/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java
index 705de23da..bca548632 100644
--- a/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java
+++ b/app/src/main/java/fr/free/nrw/commons/WelcomePagerAdapter.java
@@ -1,5 +1,6 @@
package fr.free.nrw.commons;
+import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.v4.view.PagerAdapter;
import android.view.LayoutInflater;
@@ -9,6 +10,7 @@ import android.widget.TextView;
import butterknife.ButterKnife;
import butterknife.OnClick;
+import butterknife.Optional;
public class WelcomePagerAdapter extends PagerAdapter {
static final int[] PAGE_LAYOUTS = new int[]{
@@ -20,6 +22,7 @@ public class WelcomePagerAdapter extends PagerAdapter {
};
private static final int PAGE_FINAL = 4;
private Callback callback;
+ private ViewGroup container;
/**
* Changes callback to provided one
@@ -53,6 +56,7 @@ public class WelcomePagerAdapter extends PagerAdapter {
@Override
public Object instantiateItem(ViewGroup container, int position) {
+ this.container=container;
LayoutInflater inflater = LayoutInflater.from(container.getContext());
ViewGroup layout = (ViewGroup) inflater.inflate(PAGE_LAYOUTS[position], container, false);
if( BuildConfig.FLAVOR == "beta"){
@@ -102,5 +106,15 @@ public class WelcomePagerAdapter extends PagerAdapter {
}
}
+ @Optional
+ @OnClick(R.id.welcomeInfo)
+ void onHelpClicked () {
+ try {
+ Utils.handleWebUrl(container.getContext(),Uri.parse("https://commons.wikimedia.org/wiki/Help:Contents" ));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
}
}
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 256c7e3b3..27a6f6899 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,8 +4,8 @@ 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.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
@@ -19,18 +19,16 @@ 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;
-import android.widget.Toast;
import java.io.IOException;
+import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
@@ -48,6 +46,7 @@ import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.ui.widget.HtmlTextView;
+import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
@@ -85,6 +84,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
private LoginTextWatcher textWatcher = new LoginTextWatcher();
private Boolean loginCurrentlyInProgress = false;
+ private Boolean errorMessageShown = false;
+ private String resultantError;
+ private static final String RESULTANT_ERROR = "resultantError";
+ private static final String ERROR_MESSAGE_SHOWN = "errorMessageShown";
private static final String LOGING_IN = "logingIn";
@Override
@@ -106,14 +109,14 @@ public class LoginActivity extends AccountAuthenticatorActivity {
usernameEdit.addTextChangedListener(textWatcher);
usernameEdit.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
- hideKeyboard(v);
+ ViewUtil.hideKeyboard(v);
}
});
passwordEdit.addTextChangedListener(textWatcher);
passwordEdit.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
- hideKeyboard(v);
+ ViewUtil.hideKeyboard(v);
}
});
@@ -125,13 +128,18 @@ public class LoginActivity extends AccountAuthenticatorActivity {
forgotPasswordText.setOnClickListener(view -> forgotPassword());
- if(BuildConfig.FLAVOR == "beta"){
+ if(BuildConfig.FLAVOR.equals("beta")){
loginCredentials.setText(getString(R.string.login_credential));
} else {
loginCredentials.setVisibility(View.GONE);
}
}
+ public static void startYourself(Context context) {
+ Intent intent = new Intent(context, LoginActivity.class);
+ context.startActivity(intent);
+ }
+
private void forgotPassword() {
Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL));
}
@@ -141,12 +149,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\\"));
}
- 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);
@@ -160,7 +162,10 @@ public class LoginActivity extends AccountAuthenticatorActivity {
WelcomeActivity.startYourself(this);
prefs.edit().putBoolean("firstrun", false).apply();
}
- if (sessionManager.getCurrentAccount() != null) {
+
+ if (sessionManager.getCurrentAccount() != null
+ && sessionManager.isUserLoggedIn()
+ && sessionManager.getCachedAuthCookie() != null) {
startMainActivity();
}
}
@@ -215,6 +220,8 @@ public class LoginActivity extends AccountAuthenticatorActivity {
handlePassResult(username, password);
} else {
loginCurrentlyInProgress = false;
+ errorMessageShown = true;
+ resultantError = result;
handleOtherResults(result);
}
}
@@ -266,18 +273,18 @@ public class LoginActivity extends AccountAuthenticatorActivity {
if (result.equals("NetworkFailure")) {
// Matches NetworkFailure which is created by the doInBackground method
showMessageAndCancelDialog(R.string.login_failed_network);
- } else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
+ } else if (result.toLowerCase(Locale.getDefault()).contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) {
// Matches nosuchuser, nosuchusershort, noname
- showMessageAndCancelDialog(R.string.login_failed_username);
+ showMessageAndCancelDialog(R.string.login_failed_wrong_credentials);
emptySensitiveEditFields();
- } else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) {
+ } else if (result.toLowerCase(Locale.getDefault()).contains("wrongpassword".toLowerCase())) {
// Matches wrongpassword, wrongpasswordempty
- showMessageAndCancelDialog(R.string.login_failed_password);
+ showMessageAndCancelDialog(R.string.login_failed_wrong_credentials);
emptySensitiveEditFields();
- } else if (result.toLowerCase().contains("throttle".toLowerCase())) {
+ } else if (result.toLowerCase(Locale.getDefault()).contains("throttle".toLowerCase())) {
// Matches unknown throttle error codes
showMessageAndCancelDialog(R.string.login_failed_throttled);
- } else if (result.toLowerCase().contains("userblocked".toLowerCase())) {
+ } else if (result.toLowerCase(Locale.getDefault()).contains("userblocked".toLowerCase())) {
// Matches login-userblocked
showMessageAndCancelDialog(R.string.login_failed_blocked);
} else if (result.equals("2FA")) {
@@ -341,15 +348,22 @@ public class LoginActivity extends AccountAuthenticatorActivity {
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(LOGING_IN, loginCurrentlyInProgress);
+ outState.putBoolean(ERROR_MESSAGE_SHOWN, errorMessageShown);
+ outState.putString(RESULTANT_ERROR, resultantError);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
loginCurrentlyInProgress = savedInstanceState.getBoolean(LOGING_IN, false);
+ errorMessageShown = savedInstanceState.getBoolean(ERROR_MESSAGE_SHOWN, false);
if(loginCurrentlyInProgress){
performLogin();
}
+ if(errorMessageShown){
+ resultantError = savedInstanceState.getString(RESULTANT_ERROR);
+ handleOtherResults(resultantError);
+ }
}
public void askUserForTwoFactorAuth() {
@@ -361,7 +375,9 @@ public class LoginActivity extends AccountAuthenticatorActivity {
public void showMessageAndCancelDialog(@StringRes int resId) {
showMessage(resId, R.color.secondaryDarkColor);
- progressDialog.cancel();
+ if(progressDialog != null){
+ progressDialog.cancel();
+ }
}
public void showSuccessAndDismissDialog() {
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java
index a7e62c34e..9ef6b7843 100644
--- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java
+++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java
@@ -61,13 +61,11 @@ public class SessionManager {
}
public String getAuthCookie() {
- boolean isLoggedIn = sharedPreferences.getBoolean("isUserLoggedIn", false);
-
- if (!isLoggedIn) {
+ if (!isUserLoggedIn()) {
Timber.e("User is not logged in");
return null;
} else {
- String authCookie = sharedPreferences.getString("getAuthCookie", null);
+ String authCookie = getCachedAuthCookie();
if (authCookie == null) {
Timber.e("Auth cookie is null even after login");
}
@@ -75,6 +73,20 @@ public class SessionManager {
}
}
+ public String getCachedAuthCookie() {
+ return sharedPreferences.getString("getAuthCookie", null);
+ }
+
+ public boolean isUserLoggedIn() {
+ return sharedPreferences.getBoolean("isUserLoggedIn", false);
+ }
+
+ public void forceLogin(Context context) {
+ if (context != null) {
+ LoginActivity.startYourself(context);
+ }
+ }
+
public Completable clearAllAccounts() {
AccountManager accountManager = AccountManager.get(context);
Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
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 ff6ceece4..72de0db70 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
@@ -7,18 +7,25 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
-import fr.free.nrw.commons.upload.MwVolleyApi;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import fr.free.nrw.commons.upload.GpsCategoryModel;
import timber.log.Timber;
+@Singleton
public class CacheController {
+ private final GpsCategoryModel gpsCategoryModel;
+ private final QuadTree> quadTree;
private double x, y;
- private QuadTree> quadTree;
private double xMinus, xPlus, yMinus, yPlus;
private static final int EARTH_RADIUS = 6378137;
- public CacheController() {
+ @Inject
+ CacheController(GpsCategoryModel gpsCategoryModel) {
+ this.gpsCategoryModel = gpsCategoryModel;
quadTree = new QuadTree<>(-180, -90, +180, +90);
}
@@ -31,8 +38,8 @@ public class CacheController {
public void cacheCategory() {
List pointCatList = new ArrayList<>();
- if (MwVolleyApi.GpsCatExists.getGpsCatExists()) {
- pointCatList.addAll(MwVolleyApi.getGpsCat());
+ if (gpsCategoryModel.getGpsCatExists()) {
+ pointCatList.addAll(gpsCategoryModel.getCategoryList());
Timber.d("Categories being cached: %s", pointCatList);
} else {
Timber.d("No categories found, so no categories cached");
@@ -65,7 +72,7 @@ public class CacheController {
}
//Based on algorithm at http://gis.stackexchange.com/questions/2951/algorithm-for-offsetting-a-latitude-longitude-by-some-amount-of-meters
- public void convertCoordRange() {
+ private void convertCoordRange() {
//Position, decimal degrees
double lat = y;
double lon = x;
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 a41a52139..93ddb60d5 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,7 +1,6 @@
package fr.free.nrw.commons.category;
-import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
@@ -10,14 +9,12 @@ 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;
@@ -42,9 +39,9 @@ import butterknife.ButterKnife;
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.upload.GpsCategoryModel;
import fr.free.nrw.commons.utils.StringSortingUtils;
+import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
@@ -76,6 +73,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
@Inject @Named("prefs") SharedPreferences prefsPrefs;
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
@Inject CategoryDao categoryDao;
+ @Inject GpsCategoryModel gpsCategoryModel;
private RVRendererAdapter categoriesAdapter;
private OnCategoriesSaveHandler onCategoriesSaveHandler;
@@ -118,7 +116,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
- hideKeyboard(v);
+ ViewUtil.hideKeyboard(v);
}
});
@@ -130,11 +128,6 @@ 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);
@@ -261,7 +254,6 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
}
private Observable defaultCategories() {
-
Observable directCat = directCategories();
if (hasDirectCategories) {
Timber.d("Image has direct Cat");
@@ -295,9 +287,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
}
private Observable gpsCategories() {
- return Observable.fromIterable(
- MwVolleyApi.GpsCatExists.getGpsCatExists()
- ? MwVolleyApi.getGpsCat() : new ArrayList<>())
+ return Observable.fromIterable(gpsCategoryModel.getCategoryList())
.map(name -> new CategoryItem(name, false));
}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java
index a5202046b..010e97095 100644
--- a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java
@@ -105,6 +105,7 @@ public class CategoryDao {
return items;
}
+ @NonNull
Category fromCursor(Cursor cursor) {
// Hardcoding column positions!
return new Category(
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java
new file mode 100644
index 000000000..3495d710c
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java
@@ -0,0 +1,29 @@
+package fr.free.nrw.commons.category;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.mwapi.MediaWikiApi;
+
+@Singleton
+public class CategoryImageController {
+
+ private MediaWikiApi mediaWikiApi;
+
+ @Inject
+ public CategoryImageController(MediaWikiApi mediaWikiApi) {
+ this.mediaWikiApi = mediaWikiApi;
+ }
+
+ /**
+ * Takes a category name as input and calls the API to get a list of images for that category
+ * @param categoryName
+ * @return
+ */
+ public List getCategoryImages(String categoryName) {
+ return mediaWikiApi.getCategoryImages(categoryName);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java
new file mode 100644
index 000000000..18749847e
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java
@@ -0,0 +1,225 @@
+package fr.free.nrw.commons.category;
+
+import org.jsoup.Jsoup;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import fr.free.nrw.commons.Media;
+import timber.log.Timber;
+
+public class CategoryImageUtils {
+
+ /**
+ * The method iterates over the child nodes to return a list of Media objects
+ * @param childNodes
+ * @return
+ */
+ public static List getMediaList(NodeList childNodes) {
+ List categoryImages = new ArrayList<>();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Node node = childNodes.item(i);
+ categoryImages.add(getMediaFromPage(node));
+ }
+
+ return categoryImages;
+ }
+
+ /**
+ * Creates a new Media object from the XML response as received by the API
+ * @param node
+ * @return
+ */
+ private static Media getMediaFromPage(Node node) {
+ Media media = new Media(null,
+ getImageUrl(node),
+ getFileName(node),
+ getDescription(node),
+ getDataLength(node),
+ getDateCreated(node),
+ getDateCreated(node),
+ getCreator(node)
+ );
+
+ media.setLicense(getLicense(node));
+
+ return media;
+ }
+
+ /**
+ * Extracts the filename of the uploaded image
+ * @param document
+ * @return
+ */
+ private static String getFileName(Node document) {
+ Element element = (Element) document;
+ return element.getAttribute("title");
+ }
+
+ /**
+ * Extracts the image description for that particular upload
+ * @param document
+ * @return
+ */
+ private static String getDescription(Node document) {
+ return getMetaDataValue(document, "ImageDescription");
+ }
+
+ /**
+ * Extracts license information from the image meta data
+ * @param document
+ * @return
+ */
+ private static String getLicense(Node document) {
+ return getMetaDataValue(document, "License");
+ }
+
+ /**
+ * Returns the parsed value of artist from the response
+ * The artist information is returned as a HTML string from the API. Jsoup library parses the HTML string
+ * to extract just the text value
+ * @param document
+ * @return
+ */
+ private static String getCreator(Node document) {
+ String artist = getMetaDataValue(document, "Artist");
+ if (artist != null) {
+ return Jsoup.parse(artist).text();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the parsed date of creation of the image
+ * @param document
+ * @return
+ */
+ private static Date getDateCreated(Node document) {
+ String dateTime = getMetaDataValue(document, "DateTime");
+ if (dateTime != null && !dateTime.equals("")) {
+ SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ try {
+ return format.parse(dateTime);
+ } catch (ParseException e) {
+ Timber.d("Error occurred while parsing date %s", dateTime);
+ return new Date();
+ }
+ }
+ return new Date();
+ }
+
+ /**
+ * @param document
+ * @return Returns the url attribute from the imageInfo node
+ */
+ private static String getImageUrl(Node document) {
+ Element element = (Element) getImageInfo(document);
+ if (element != null) {
+ return element.getAttribute("url");
+ }
+ return null;
+ }
+
+ /**
+ * Takes the node document and gives out the attribute length from the node document
+ * @param document
+ * @return
+ */
+ private static long getDataLength(Node document) {
+ Element element = (Element) document;
+ if (element != null) {
+ String length = element.getAttribute("length");
+ if (length != null && !length.equals("")) {
+ return Long.parseLong(length);
+ }
+ }
+ return 0L;
+ }
+
+ /**
+ * Generic method to get the value of any meta as returned by the getMetaData function
+ * @param document node document as returned by API
+ * @param metaName the name of meta node to be returned
+ * @return
+ */
+ private static String getMetaDataValue(Node document, String metaName) {
+ Element metaData = getMetaData(document, metaName);
+ if (metaData != null) {
+ return metaData.getAttribute("value");
+ }
+ return null;
+ }
+
+ /**
+ * Generic method to return an element taking the node document and metaName as input
+ * @param document node document as returned by API
+ * @param metaName the name of meta node to be returned
+ * @return
+ */
+ @Nullable
+ private static Element getMetaData(Node document, String metaName) {
+ Node extraMetaData = getExtraMetaData(document);
+ if (extraMetaData != null) {
+ Node node = getNode(extraMetaData, metaName);
+ if (node != null) {
+ return (Element) node;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Extracts extmetadata from the response XML
+ * @param document
+ * @return
+ */
+ @Nullable
+ private static Node getExtraMetaData(Node document) {
+ Node imageInfo = getImageInfo(document);
+ if (imageInfo != null) {
+ return getNode(imageInfo, "extmetadata");
+ }
+ return null;
+ }
+
+ /**
+ * Extracts the ii node from the imageinfo node
+ * @param document
+ * @return
+ */
+ @Nullable
+ private static Node getImageInfo(Node document) {
+ Node imageInfo = getNode(document, "imageinfo");
+ if (imageInfo != null) {
+ return getNode(imageInfo, "ii");
+ }
+ return null;
+ }
+
+ /**
+ * Takes a parent node as input and returns a child node if present
+ * @param node parent node
+ * @param nodeName child node name
+ * @return
+ */
+ @Nullable
+ public static Node getNode(Node node, String nodeName) {
+ NodeList childNodes = node.getChildNodes();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Node nodeItem = childNodes.item(i);
+ Element item = (Element) nodeItem;
+ if (item.getTagName().equals(nodeName)) {
+ return nodeItem;
+ }
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java
new file mode 100644
index 000000000..17601151c
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java
@@ -0,0 +1,159 @@
+package fr.free.nrw.commons.category;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.DataSetObserver;
+import android.os.Bundle;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.view.View;
+import android.widget.AdapterView;
+
+import butterknife.ButterKnife;
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.auth.AuthenticatedActivity;
+import fr.free.nrw.commons.media.MediaDetailPagerFragment;
+
+/**
+ * This activity displays pictures of a particular category
+ * Its generic and simply takes the name of category name in its start intent to load all images in
+ * a particular category. This activity is currently being used to display a list of featured images,
+ * which is nothing but another category on wikimedia commons.
+ */
+
+public class CategoryImagesActivity
+ extends AuthenticatedActivity
+ implements FragmentManager.OnBackStackChangedListener,
+ MediaDetailPagerFragment.MediaDetailProvider,
+ AdapterView.OnItemClickListener{
+
+
+ private FragmentManager supportFragmentManager;
+ private CategoryImagesListFragment categoryImagesListFragment;
+ private MediaDetailPagerFragment mediaDetails;
+
+ @Override
+ protected void onAuthCookieAcquired(String authCookie) {
+
+ }
+
+ @Override
+ protected void onAuthFailure() {
+
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_category_images);
+ ButterKnife.bind(this);
+
+ // Activity can call methods in the fragment by acquiring a
+ // reference to the Fragment from FragmentManager, using findFragmentById()
+ supportFragmentManager = getSupportFragmentManager();
+ setCategoryImagesFragment();
+ supportFragmentManager.addOnBackStackChangedListener(this);
+ if (savedInstanceState != null) {
+ mediaDetails = (MediaDetailPagerFragment) supportFragmentManager
+ .findFragmentById(R.id.fragmentContainer);
+
+ }
+ requestAuthToken();
+ initDrawer();
+ setPageTitle();
+ }
+
+ /**
+ * Gets the categoryName from the intent and initializes the fragment for showing images of that category
+ */
+ private void setCategoryImagesFragment() {
+ categoryImagesListFragment = new CategoryImagesListFragment();
+ String categoryName = getIntent().getStringExtra("categoryName");
+ if (getIntent() != null && categoryName != null) {
+ Bundle arguments = new Bundle();
+ arguments.putString("categoryName", categoryName);
+ categoryImagesListFragment.setArguments(arguments);
+ FragmentTransaction transaction = supportFragmentManager.beginTransaction();
+ transaction
+ .add(R.id.fragmentContainer, categoryImagesListFragment)
+ .commit();
+ }
+ }
+
+ /**
+ * Gets the passed title from the intents and displays it as the page title
+ */
+ private void setPageTitle() {
+ if (getIntent() != null && getIntent().getStringExtra("title") != null) {
+ setTitle(getIntent().getStringExtra("title"));
+ }
+ }
+
+ @Override
+ public void onBackStackChanged() {
+ }
+
+ @Override
+ public void onItemClick(AdapterView> adapterView, View view, int i, long l) {
+ if (mediaDetails == null || !mediaDetails.isVisible()) {
+ // set isFeaturedImage true for featured images, to include author field on media detail
+ mediaDetails = new MediaDetailPagerFragment(false, true);
+ FragmentManager supportFragmentManager = getSupportFragmentManager();
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.fragmentContainer, mediaDetails)
+ .addToBackStack(null)
+ .commit();
+ supportFragmentManager.executePendingTransactions();
+ }
+ mediaDetails.showImage(i);
+ }
+
+ /**
+ * Consumers should be simply using this method to use this activity.
+ * @param context
+ * @param title Page title
+ * @param categoryName Name of the category for displaying its images
+ */
+ public static void startYourself(Context context, String title, String categoryName) {
+ Intent intent = new Intent(context, CategoryImagesActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ intent.putExtra("title", title);
+ intent.putExtra("categoryName", categoryName);
+ context.startActivity(intent);
+ }
+
+ @Override
+ public Media getMediaAtPosition(int i) {
+ if (categoryImagesListFragment.getAdapter() == null) {
+ // not yet ready to return data
+ return null;
+ } else {
+ return (Media) categoryImagesListFragment.getAdapter().getItem(i);
+ }
+ }
+
+ @Override
+ public int getTotalMediaCount() {
+ if (categoryImagesListFragment.getAdapter() == null) {
+ return 0;
+ }
+ return categoryImagesListFragment.getAdapter().getCount();
+ }
+
+ @Override
+ public void notifyDatasetChanged() {
+
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java
new file mode 100644
index 000000000..a44e19a29
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java
@@ -0,0 +1,237 @@
+package fr.free.nrw.commons.category;
+
+import android.annotation.SuppressLint;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.GridView;
+import android.widget.ListAdapter;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import dagger.android.support.DaggerFragment;
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.utils.NetworkUtils;
+import fr.free.nrw.commons.utils.ViewUtil;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.schedulers.Schedulers;
+import timber.log.Timber;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+/**
+ * Displays images for a particular category with load more on scrolling incorporated
+ */
+public class CategoryImagesListFragment extends DaggerFragment {
+
+ private static int TIMEOUT_SECONDS = 15;
+
+ private GridViewAdapter gridAdapter;
+
+ @BindView(R.id.statusMessage)
+ TextView statusTextView;
+ @BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar;
+ @BindView(R.id.categoryImagesList) GridView gridView;
+
+ private boolean hasMoreImages = true;
+ private boolean isLoading;
+ private String categoryName = null;
+
+ @Inject CategoryImageController controller;
+ @Inject @Named("category_prefs") SharedPreferences categoryPreferences;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.fragment_category_images, container, false);
+ ButterKnife.bind(this, v);
+ return v;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
+ initViews();
+ }
+
+ /**
+ * Initializes the UI elements for the fragment
+ * Setup the grid view to and scroll listener for it
+ */
+ private void initViews() {
+ String categoryName = getArguments().getString("categoryName");
+ if (getArguments() != null && categoryName != null) {
+ this.categoryName = categoryName;
+ resetQueryContinueValues(categoryName);
+ initList();
+ setScrollListener();
+ }
+ }
+
+ /**
+ * Query continue values determine the last page that was loaded for the particular keyword
+ * This method resets those values, so that the results can be queried from the first page itself
+ * @param keyword
+ */
+ private void resetQueryContinueValues(String keyword) {
+ SharedPreferences.Editor editor = categoryPreferences.edit();
+ editor.remove(keyword);
+ editor.apply();
+ }
+
+ /**
+ * Checks for internet connection and then initializes the grid view with first 10 images of that category
+ */
+ @SuppressLint("CheckResult")
+ private void initList() {
+ if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
+ handleNoInternet();
+ return;
+ }
+
+ isLoading = true;
+ progressBar.setVisibility(VISIBLE);
+ Observable.fromCallable(() -> controller.getCategoryImages(categoryName))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .subscribe(this::handleSuccess, this::handleError);
+ }
+
+ /**
+ * Handles the UI updates for no internet scenario
+ */
+ private void handleNoInternet() {
+ progressBar.setVisibility(GONE);
+ if (gridAdapter == null || gridAdapter.isEmpty()) {
+ statusTextView.setVisibility(VISIBLE);
+ statusTextView.setText(getString(R.string.no_internet));
+ } else {
+ ViewUtil.showSnackbar(gridView, R.string.no_internet);
+ }
+ }
+
+ /**
+ * Logs and handles API error scenario
+ * @param throwable
+ */
+ private void handleError(Throwable throwable) {
+ Timber.e(throwable, "Error occurred while loading featured images");
+ initErrorView();
+ }
+
+ /**
+ * Handles the UI updates for a error scenario
+ */
+ private void initErrorView() {
+ ViewUtil.showSnackbar(gridView, R.string.error_loading_images);
+ progressBar.setVisibility(GONE);
+ if (gridAdapter == null || gridAdapter.isEmpty()) {
+ statusTextView.setVisibility(VISIBLE);
+ statusTextView.setText(getString(R.string.no_images_found));
+ } else {
+ statusTextView.setVisibility(GONE);
+ }
+ }
+
+ /**
+ * Initializes the adapter with a list of Media objects
+ * @param mediaList
+ */
+ private void setAdapter(List mediaList) {
+ gridAdapter = new GridViewAdapter(this.getContext(), R.layout.layout_category_images, mediaList);
+ gridView.setAdapter(gridAdapter);
+ }
+
+ /**
+ * Sets the scroll listener for the grid view so that more images are fetched when the user scrolls down
+ * Checks if the category has more images before loading
+ * Also checks whether images are currently being fetched before triggering another request
+ */
+ private void setScrollListener() {
+ gridView.setOnScrollListener(new AbsListView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ if (hasMoreImages && !isLoading && (firstVisibleItem + visibleItemCount + 1 >= totalItemCount)) {
+ isLoading = true;
+ fetchMoreImages();
+ }
+ }
+ });
+ }
+
+ /**
+ * Fetches more images for the category and adds it to the grid view adapter
+ */
+ @SuppressLint("CheckResult")
+ private void fetchMoreImages() {
+ if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
+ handleNoInternet();
+ return;
+ }
+
+ progressBar.setVisibility(VISIBLE);
+ Observable.fromCallable(() -> controller.getCategoryImages(categoryName))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .subscribe(this::handleSuccess, this::handleError);
+ }
+
+ /**
+ * Handles the success scenario
+ * On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
+ * @param collection
+ */
+ private void handleSuccess(List collection) {
+ if(collection == null || collection.isEmpty()) {
+ initErrorView();
+ hasMoreImages = false;
+ return;
+ }
+
+ if(gridAdapter == null) {
+ setAdapter(collection);
+ } else {
+ gridAdapter.addItems(collection);
+ }
+
+ progressBar.setVisibility(GONE);
+ isLoading = false;
+ statusTextView.setVisibility(GONE);
+ }
+
+ public ListAdapter getAdapter() {
+ return gridView.getAdapter();
+ }
+
+ /**
+ * This method will be called on back pressed of CategoryImagesActivity.
+ * It initializes the grid view by setting adapter.
+ */
+ @Override
+ public void onResume() {
+ gridView.setAdapter(gridAdapter);
+ super.onResume();
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java
new file mode 100644
index 000000000..c8e6066f6
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java
@@ -0,0 +1,88 @@
+package fr.free.nrw.commons.category;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.MediaWikiImageView;
+import fr.free.nrw.commons.R;
+
+/**
+ * This is created to only display UI implementation. Needs to be changed in real implementation
+ */
+
+public class GridViewAdapter extends ArrayAdapter {
+ private Context context;
+ private List data;
+
+ public GridViewAdapter(Context context, int layoutResourceId, List data) {
+ super(context, layoutResourceId, data);
+ this.context = context;
+ this.data = data;
+ }
+
+ /**
+ * Adds more item to the list
+ * Its triggered on scrolling down in the list
+ * @param images
+ */
+ public void addItems(List images) {
+ if (data == null) {
+ data = new ArrayList<>();
+ }
+ data.addAll(images);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return data == null || data.isEmpty();
+ }
+
+ /**
+ * Sets up the UI for the category image item
+ * @param position
+ * @param convertView
+ * @param parent
+ * @return
+ */
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+
+ if (convertView == null) {
+ LayoutInflater inflater = ((Activity) context).getLayoutInflater();
+ convertView = inflater.inflate(R.layout.layout_category_images, null);
+ }
+
+ Media item = data.get(position);
+ MediaWikiImageView imageView = convertView.findViewById(R.id.categoryImageView);
+ TextView fileName = convertView.findViewById(R.id.categoryImageTitle);
+ TextView author = convertView.findViewById(R.id.categoryImageAuthor);
+ fileName.setText(item.getFilename());
+ setAuthorView(item, author);
+ imageView.setMedia(item);
+ return convertView;
+ }
+
+ /**
+ * Shows author information if its present
+ * @param item
+ * @param author
+ */
+ private void setAuthorView(Media item, TextView author) {
+ if (item.getCreator() != null && !item.getCreator().equals("")) {
+ String uploadedByTemplate = context.getString(R.string.image_uploaded_by);
+ author.setText(String.format(uploadedByTemplate, item.getCreator()));
+ } else {
+ author.setVisibility(View.GONE);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/category/QueryContinue.java b/app/src/main/java/fr/free/nrw/commons/category/QueryContinue.java
new file mode 100644
index 000000000..e12d5a778
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/QueryContinue.java
@@ -0,0 +1,24 @@
+package fr.free.nrw.commons.category;
+
+/**
+ * For APIs that return paginated responses, MediaWiki APIs uses the QueryContinue to facilitate fetching of subsequent pages
+ * https://www.mediawiki.org/wiki/API:Raw_query_continue
+ */
+public class QueryContinue {
+ private String continueParam;
+ private String gcmContinueParam;
+
+ public QueryContinue(String continueParam, String gcmContinueParam) {
+ this.continueParam = continueParam;
+ this.gcmContinueParam = gcmContinueParam;
+ }
+
+ public String getGcmContinueParam() {
+ return gcmContinueParam;
+ }
+
+ public String getContinueParam() {
+ return continueParam;
+ }
+}
+
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 7861f96de..99009c029 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
@@ -45,6 +45,7 @@ public class Contribution extends Media {
private long transferred;
private String decimalCoords;
private boolean isMultiple;
+ private String wikiDataEntityId;
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp,
int state, long dataLength, Date dateUploaded, long transferred,
@@ -222,4 +223,17 @@ public class Contribution extends Media {
throw new RuntimeException("Unrecognized license value: " + license);
}
+
+ public String getWikiDataEntityId() {
+ return wikiDataEntityId;
+ }
+
+ /**
+ * When the corresponding wikidata entity is known as in case of nearby uploads, it can be set
+ * using the setter method
+ * @param wikiDataEntityId
+ */
+ public void setWikiDataEntityId(String wikiDataEntityId) {
+ this.wikiDataEntityId = wikiDataEntityId;
+ }
}
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 37b3d5377..ed6001f94 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
@@ -90,7 +90,7 @@ public class ContributionController {
fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY);
}
- public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload) {
+ public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload, String wikiDataEntityId) {
FragmentActivity activity = fragment.getActivity();
Timber.d("handleImagePicked() called with onActivityResult()");
Intent shareIntent = new Intent(activity, ShareActivity.class);
@@ -102,9 +102,6 @@ public class ContributionController {
shareIntent.setType(activity.getContentResolver().getType(imageData));
shareIntent.putExtra(EXTRA_STREAM, imageData);
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY);
- if (isDirectUpload) {
- shareIntent.putExtra("isDirectUpload", true);
- }
break;
case SELECT_FROM_CAMERA:
//FIXME: Find out appropriate mime type
@@ -113,9 +110,6 @@ public class ContributionController {
shareIntent.setType("image/jpeg");
shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri);
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA);
- if (isDirectUpload) {
- shareIntent.putExtra("isDirectUpload", true);
- }
break;
default:
@@ -123,6 +117,10 @@ public class ContributionController {
}
Timber.i("Image selected");
try {
+ shareIntent.putExtra("isDirectUpload", isDirectUpload);
+ if (wikiDataEntityId != null && !wikiDataEntityId.equals("")) {
+ shareIntent.putExtra("wikiDataEntityId", wikiDataEntityId);
+ }
activity.startActivity(shareIntent);
} catch (SecurityException e) {
Timber.e(e, "Security Exception");
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java
index 079cf6477..6d290b1a5 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java
@@ -8,7 +8,6 @@ import android.net.Uri;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.text.TextUtils;
-import android.util.Log;
import java.util.Date;
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 ff400a8dd..0b600c5d0 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
@@ -117,7 +117,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);
- controller.handleImagePicked(requestCode, data, false);
+ controller.handleImagePicked(requestCode, data, false, null);
} else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);
diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java
index 0cce496f0..37b9a7a82 100644
--- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java
+++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteTask.java
@@ -83,7 +83,7 @@ public class DeleteTask extends AsyncTask {
String logPageString = "\n{{Commons:Deletion requests/" + media.getFilename() +
"}}\n";
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd", Locale.getDefault());
String date = sdf.format(calendar.getTime());
String userPageString = "\n{{subst:idw|" + media.getFilename() +
diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
index e4fb13427..51aa85903 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
@@ -7,6 +7,7 @@ import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
+import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
@@ -46,4 +47,7 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract NotificationActivity bindNotificationActivity();
+
+ @ContributesAndroidInjector
+ abstract CategoryImagesActivity bindFeaturedImagesActivity();
}
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 91f6d4ccb..43721a217 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
@@ -9,17 +9,18 @@ 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.contributions.Contribution;
-import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.contributions.ContributionsSyncAdapter;
import fr.free.nrw.commons.delete.DeleteTask;
import fr.free.nrw.commons.modifications.ModificationsSyncAdapter;
-import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.nearby.PlaceRenderer;
+import fr.free.nrw.commons.upload.FileProcessor;
+import fr.free.nrw.commons.settings.SettingsFragment;
+
@Singleton
@Component(modules = {
CommonsApplicationModule.class,
+ NetworkingModule.class,
AndroidInjectionModule.class,
AndroidSupportInjectionModule.class,
ActivityBuilderModule.class,
@@ -47,6 +48,8 @@ public interface CommonsApplicationComponent extends AndroidInjector provideLruCache() {
return new LruCache<>(1024);
}
+
+ @Provides
+ @Singleton
+ public WikidataEditListener provideWikidataEditListener() {
+ return new WikidataEditListenerImpl();
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
index a94f46ca9..dfed64871 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
@@ -4,6 +4,7 @@ import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.contributions.ContributionsListFragment;
+import fr.free.nrw.commons.category.CategoryImagesListFragment;
import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.nearby.NearbyListFragment;
@@ -47,4 +48,7 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract SingleUploadFragment bindSingleUploadFragment();
+ @ContributesAndroidInjector
+ abstract CategoryImagesListFragment bindFeaturedImagesListFragment();
+
}
diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java
new file mode 100644
index 000000000..cd043e950
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java
@@ -0,0 +1,59 @@
+package fr.free.nrw.commons.di;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.support.annotation.NonNull;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+import fr.free.nrw.commons.BuildConfig;
+import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
+import fr.free.nrw.commons.mwapi.MediaWikiApi;
+import okhttp3.HttpUrl;
+import okhttp3.OkHttpClient;
+
+@Module
+@SuppressWarnings({"WeakerAccess", "unused"})
+public class NetworkingModule {
+ public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
+
+ @Provides
+ @Singleton
+ public OkHttpClient provideOkHttpClient() {
+ return new OkHttpClient.Builder().build();
+ }
+
+ @Provides
+ @Singleton
+ public MediaWikiApi provideMediaWikiApi(Context context,
+ @Named("default_preferences") SharedPreferences defaultPreferences,
+ @Named("category_prefs") SharedPreferences categoryPrefs,
+ Gson gson) {
+ return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultPreferences, categoryPrefs, gson);
+ }
+
+ @Provides
+ @Named("commons_mediawiki_url")
+ @NonNull
+ @SuppressWarnings("ConstantConditions")
+ public HttpUrl provideMwUrl() {
+ return HttpUrl.parse(BuildConfig.COMMONS_URL);
+ }
+
+ /**
+ * Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere.
+ * @return returns a singleton Gson instance
+ */
+ @Provides
+ @Singleton
+ public Gson provideGson() {
+ return new GsonBuilder().create();
+ }
+
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/glide/SvgDecoder.java b/app/src/main/java/fr/free/nrw/commons/glide/SvgDecoder.java
new file mode 100644
index 000000000..9087f9501
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/glide/SvgDecoder.java
@@ -0,0 +1,36 @@
+package fr.free.nrw.commons.glide;
+
+import android.support.annotation.NonNull;
+
+import com.bumptech.glide.load.Options;
+import com.bumptech.glide.load.ResourceDecoder;
+import com.bumptech.glide.load.engine.Resource;
+import com.bumptech.glide.load.resource.SimpleResource;
+import com.caverock.androidsvg.SVG;
+import com.caverock.androidsvg.SVGParseException;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Decodes an SVG internal representation from an {@link InputStream}.
+ */
+public class SvgDecoder implements ResourceDecoder {
+
+ @Override
+ public boolean handles(@NonNull InputStream source, @NonNull Options options) {
+ // TODO: Can we tell?
+ return true;
+ }
+
+ public Resource