redirecting to category images Actovoty instead of Nearby now

This commit is contained in:
Ujjwal Agrawal 2018-07-31 12:05:48 +05:30
commit 68165c7ea6
271 changed files with 10556 additions and 2001 deletions

View file

@ -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-.+'

View file

@ -1,5 +1,8 @@
# 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

View file

@ -32,3 +32,7 @@ The body should provide a meaningful commit message.
1. Write tests for your code (if possible)
1. Make sure the Wiki pages don't become stale by updating them (if needed)
### Further reading
* [Importance of good commit messages](https://blog.oozou.com/commit-messages-matter-60309983c227?gi=c550a10d0f67)

View file

@ -1,25 +1,18 @@
_Before creating an issue, please search the existing issues to see if a similar one has already been created. You can search issues by specific labels (e.g. `label:nearby `) or just by typing keywords into the search filter._
**Summary:**
Summarize your issue in one sentence (what goes wrong, what did you expect to happen)
_Before creating an issue, please search the existing issues to see if a similar one has already been created. You can search issues by specific labels (e.g. `label:nearby `) or just by typing keywords into the search filter._
**Steps to reproduce:**
How can we reproduce the issue?
How can we reproduce the issue?
What did you expect the app to do, and what did you see instead?
**Add System logs:**
Add logcat files here (if possible).
**Expected behavior:**
What did you expect the App to do?
**Observed behavior:**
What did you see instead? Describe your issue in detail here.
**Device and Android version:**
What make and model device (e.g., Samsung J7) did you encounter this on? What Android
@ -28,7 +21,7 @@ version (e.g., Android 4.0 Ice Cream Sandwich or Android 6.0 Marshmallow) are yo
**Commons app version:**
You can find this information by going to the navigation drawer in the app and tapping 'About'
You can find this information by going to the navigation drawer in the app and tapping 'About'. If you are building from our codebase instead of downloading the app, please also mention the branch and build variant (e.g. master and prodDebug).
**Screen-shots:**

View file

@ -1,3 +1,7 @@
## Title (required)
Fixes #{GitHub issue number and title (Please do not forget adding title) }
## Description (required)
Fixes #{GitHub issue number and title}
@ -12,4 +16,4 @@ Tested on {API level & name of device/emulator}, with {build variant, e.g. ProdD
{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._
_Note: Please ensure that you have read CONTRIBUTING.md if this is your first pull request._

View file

@ -7,11 +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.github.chrisbanes:PhotoView:2.0.0'
implementation 'com.android.volley:volley:1.0.0'
implementation 'ch.acra:acra:4.9.2'
implementation 'org.mediawiki:api:1.3'
implementation 'commons-codec:commons-codec:1.10'
@ -20,59 +21,57 @@ dependencies {
implementation 'com.jakewharton.timber:timber:4.5.1'
implementation 'info.debatty:java-string-similarity:0.24'
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.android.support.constraint:constraint-layout:1.1.0'
implementation('com.mapbox.mapboxsdk:mapbox-android-sdk:5.5.0@aar') {
transitive = true
}
implementation "com.github.deano2390:MaterialShowcaseView:1.2.0"
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
//noinspection GradleCompatible
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.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'
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.robolectric:multidex:3.4.2'
testImplementation "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 'com.nhaarman:mockito-kotlin:1.5.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1'
implementation 'com.dinuscxj:circleprogressbar:1.1.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.2-alpha1'
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"
@ -87,8 +86,8 @@ android {
defaultConfig {
applicationId 'fr.free.nrw.commons'
versionCode 84
versionName '2.7.1'
versionCode 85
versionName '2.7.2'
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
minSdkVersion project.minSdkVersion
@ -117,7 +116,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"
@ -129,7 +128,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/\""
@ -145,7 +146,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/\""

BIN
app/libs/java-json.jar Normal file

Binary file not shown.

9
app/proguard-glide.txt Normal file
View file

@ -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

View file

@ -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 { *; }

View file

@ -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")
}
}
}

View file

@ -1,3 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="fr.free.nrw.commons">
@ -15,6 +16,7 @@
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
<uses-permission android:name="android.permission.READ_LOGS"/>
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
<!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
<uses-feature android:name="android.hardware.location.gps" />
@ -26,10 +28,10 @@
android:theme="@style/LightAppTheme"
android:supportsRtl="true" >
<activity android:name="org.acra.CrashReportDialog"
android:theme="@android:style/Theme.Dialog"
android:launchMode="singleInstance"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true" />
android:theme="@android:style/Theme.Dialog"
android:launchMode="singleInstance"
android:excludeFromRecents="true"
android:finishOnTaskLaunch="true" />
<activity android:name=".auth.LoginActivity">
<intent-filter>
@ -44,7 +46,7 @@
android:name=".upload.ShareActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
<intent-filter>
<intent-filter android:label="@string/intent_share_upload_label">
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
@ -56,7 +58,7 @@
android:name=".upload.MultipleShareActivity"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
<intent-filter>
<intent-filter android:label="@string/intent_share_upload_label">
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
@ -96,8 +98,22 @@
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.ContributionsActivity" />
<service android:name=".upload.UploadService" />
<activity
android:name=".category.CategoryDetailsActivity"
android:label="@string/title_activity_featured_images"
android:parentActivityName=".contributions.ContributionsActivity" />
<activity
android:name=".explore.SearchActivity"
android:label="@string/title_activity_search"
android:parentActivityName=".contributions.ContributionsActivity"
/>
<activity
android:name=".achievements.AchievementsActivity"
android:label="@string/Achievements" />
<service android:name=".upload.UploadService" />
<service
android:name=".auth.WikiAccountAuthenticatorService"
android:exported="true"
@ -164,6 +180,23 @@
android:label="@string/provider_categories"
android:syncable="false" />
<provider
android:name=".explore.recentsearches.RecentSearchesContentProvider"
android:authorities="fr.free.nrw.commons.explore.recentsearches.contentprovider"
android:exported="false"
android:label="@string/provider_searches"
android:syncable="false" />
<receiver android:name=".widget.PicOfDayAppWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/pic_of_day_app_widget_info" />
</receiver>
</application>
</manifest>
</manifest>

View file

@ -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
*/

View file

@ -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;

View file

@ -3,13 +3,17 @@ package fr.free.nrw.commons;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -356,12 +360,8 @@ public class Media implements Parcelable {
* @param descriptions Media descriptions
*/
void setDescriptions(Map<String, String> descriptions) {
for (String key : this.descriptions.keySet()) {
this.descriptions.remove(key);
}
for (String key : descriptions.keySet()) {
this.descriptions.put(key, descriptions.get(key));
}
this.descriptions.clear();
this.descriptions.putAll(descriptions);
}
/**

View file

@ -51,10 +51,16 @@ public class MediaWikiImageView extends SimpleDraweeView {
return;
}
if (thumbnailUrlCache.get(media.getFilename()) != null) {
setImageUrl(thumbnailUrlCache.get(media.getFilename()));
} else {
setImageUrl(null);
if(media.getFilename() != null) {
if (thumbnailUrlCache.get(media.getFilename()) != null) {
setImageUrl(thumbnailUrlCache.get(media.getFilename()));
} else {
setImageUrl(null);
currentThumbnailTask = new ThumbnailFetchTask(media, mwApi);
currentThumbnailTask.execute(media.getFilename());
}
} else { // local image
setImageUrl(media.getLocalUri().toString());
currentThumbnailTask = new ThumbnailFetchTask(media, mwApi);
currentThumbnailTask.execute(media.getFilename());
}

View file

@ -2,11 +2,13 @@ package fr.free.nrw.commons;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.customtabs.CustomTabsIntent;
import android.support.v4.content.ContextCompat;
import android.view.View;
import android.widget.Toast;
import org.apache.commons.codec.binary.Hex;
@ -76,7 +78,11 @@ public class Utils {
* @return string with capitalized first character
*/
public static String capitalize(String string) {
return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1);
if(string.length() > 0) {
return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1);
} else {
return string;
}
}
/**
@ -146,7 +152,7 @@ public class Utils {
StringBuilder stringBuilder = new StringBuilder();
try {
String[] command = new String[] {"logcat","-d","-v","threadtime"};
String[] command = new String[]{"logcat","-d","-v","threadtime"};
Process process = Runtime.getRuntime().exec(command);
@ -178,6 +184,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);
@ -194,4 +201,18 @@ public class Utils {
customTabsIntent.launchUrl(context, url);
}
/**
* To take screenshot of the screen and return it in Bitmap format
*
* @param view
* @return
*/
public static Bitmap getScreenShot(View view) {
View screenView = view.getRootView();
screenView.setDrawingCacheEnabled(true);
Bitmap bitmap = Bitmap.createBitmap(screenView.getDrawingCache());
screenView.setDrawingCacheEnabled(false);
return bitmap;
}
}

View file

@ -0,0 +1,205 @@
package fr.free.nrw.commons.achievements;
import android.util.Log;
/**
* represnts Achievements class ans stores all the parameters
*/
public class Achievements {
private int uniqueUsedImages;
private int articlesUsingImages;
private int thanksReceived;
private int imagesEditedBySomeoneElse;
private int featuredImages;
private int imagesUploaded;
private int revertCount;
public Achievements(){
}
/**
* constructor for achievements class to set its data members
* @param uniqueUsedImages
* @param articlesUsingImages
* @param thanksReceived
* @param imagesEditedBySomeoneElse
* @param featuredImages
* @param imagesUploaded
* @param revertCount
*/
public Achievements(int uniqueUsedImages,
int articlesUsingImages,
int thanksReceived,
int imagesEditedBySomeoneElse,
int featuredImages,
int imagesUploaded,
int revertCount) {
this.uniqueUsedImages = uniqueUsedImages;
this.articlesUsingImages = articlesUsingImages;
this.thanksReceived = thanksReceived;
this.imagesEditedBySomeoneElse = imagesEditedBySomeoneElse;
this.featuredImages = featuredImages;
this.imagesUploaded = imagesUploaded;
this.revertCount = revertCount;
}
/**
* Builder class for Achievements class
*/
public class AchievementsBuilder {
private int nestedUniqueUsedImages;
private int nestedArticlesUsingImages;
private int nestedThanksReceived;
private int nestedImagesEditedBySomeoneElse;
private int nestedFeaturedImages;
private int nestedImagesUploaded;
private int nestedRevertCount;
public AchievementsBuilder setUniqueUsedImages(int uniqueUsedImages) {
this.nestedUniqueUsedImages = uniqueUsedImages;
return this;
}
public AchievementsBuilder setArticlesUsingImages(int articlesUsingImages) {
this.nestedArticlesUsingImages = articlesUsingImages;
return this;
}
public AchievementsBuilder setThanksReceived(int thanksReceived) {
this.nestedThanksReceived = thanksReceived;
return this;
}
public AchievementsBuilder setImagesEditedBySomeoneElse(int imagesEditedBySomeoneElse) {
this.nestedImagesEditedBySomeoneElse = imagesEditedBySomeoneElse;
return this;
}
public AchievementsBuilder setFeaturedImages(int featuredImages) {
this.nestedFeaturedImages = featuredImages;
return this;
}
public AchievementsBuilder setImagesUploaded(int imagesUploaded) {
this.nestedImagesUploaded = imagesUploaded;
return this;
}
public AchievementsBuilder setRevertCount( int revertCount){
this.nestedRevertCount = revertCount;
return this;
}
public Achievements createAchievements(){
return new Achievements(nestedUniqueUsedImages,
nestedArticlesUsingImages,
nestedThanksReceived,
nestedImagesEditedBySomeoneElse,
nestedFeaturedImages,
nestedImagesUploaded,
nestedRevertCount);
}
}
/**
* getter function to get count of images uploaded
* @return
*/
public int getImagesUploaded() {
return imagesUploaded;
}
/**
* getter function to get count of featured images
* @return
*/
public int getFeaturedImages() {
return featuredImages;
}
/**
* getter function to get count of thanks received
* @return
*/
public int getThanksReceived() {
return thanksReceived;
}
/**
* getter function to get count of unique images used by wiki
* @return
*/
public int getUniqueUsedImages() {
return uniqueUsedImages;
}
/**
* setter function to count of images uploaded
* @param imagesUploaded
*/
public void setImagesUploaded(int imagesUploaded) {
this.imagesUploaded = imagesUploaded;
}
/**
* setter function to set count of featured images
* @param featuredImages
*/
public void setFeaturedImages(int featuredImages) {
this.featuredImages = featuredImages;
}
/**
* setter function to set the count of images edited by someone
* @param imagesEditedBySomeoneElse
*/
public void setImagesEditedBySomeoneElse(int imagesEditedBySomeoneElse) {
this.imagesEditedBySomeoneElse = imagesEditedBySomeoneElse;
}
/**
* setter function to set count of thanks received
* @param thanksReceived
*/
public void setThanksReceived(int thanksReceived) {
this.thanksReceived = thanksReceived;
}
/**
* setter function to count of articles using images uploaded
* @param articlesUsingImages
*/
public void setArticlesUsingImages(int articlesUsingImages) {
this.articlesUsingImages = articlesUsingImages;
}
/**
* setter function to set count of uniques images used by wiki
* @param uniqueUsedImages
*/
public void setUniqueUsedImages(int uniqueUsedImages) {
this.uniqueUsedImages = uniqueUsedImages;
}
/**
* to set count of images reverted
* @param revertCount
*/
public void setRevertCount(int revertCount) {
this.revertCount = revertCount;
}
/**
* used to calculate the percentages of images that haven't been reverted
* @return
*/
public int getNotRevertPercentage(){
try {
return ((imagesUploaded - revertCount) * 100)/imagesUploaded;
} catch (ArithmeticException divideByZero ){
return 100;
}
}
}

View file

@ -0,0 +1,458 @@
package fr.free.nrw.commons.achievements;
import android.accounts.Account;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.dinuscxj.progressbar.CircleProgressBar;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Optional;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
/**
* activity for sharing feedback on uploaded activity
*/
public class AchievementsActivity extends NavigationBaseActivity {
private static final double BADGE_IMAGE_WIDTH_RATIO = 0.4;
private static final double BADGE_IMAGE_HEIGHT_RATIO = 0.3;
private Boolean isUploadFetched = false;
private Boolean isStatisticsFetched = false;
private Boolean isRevertFetched = false;
private Achievements achievements = new Achievements();
private LevelController.LevelInfo levelInfo;
@BindView(R.id.achievement_badge)
ImageView imageView;
@BindView(R.id.achievement_level)
TextView levelNumber;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.thanks_received)
TextView thanksReceived;
@BindView(R.id.images_uploaded_progressbar)
CircleProgressBar imagesUploadedProgressbar;
@BindView(R.id.images_used_by_wiki_progressbar)
CircleProgressBar imagesUsedByWikiProgessbar;
@BindView(R.id.image_reverts_progressbar)
CircleProgressBar imageRevertsProgressbar;
@BindView(R.id.image_featured)
TextView imagesFeatured;
@BindView(R.id.images_revert_limit_text)
TextView imagesRevertLimitText;
@BindView(R.id.progressBar)
ProgressBar progressBar;
@BindView(R.id.layout_image_uploaded)
RelativeLayout layoutImageUploaded;
@BindView(R.id.layout_image_reverts)
RelativeLayout layoutImageReverts;
@BindView(R.id.layout_image_used_by_wiki)
RelativeLayout layoutImageUsedByWiki;
@BindView(R.id.layout_statistics)
LinearLayout layoutStatistics;
@Inject
SessionManager sessionManager;
@Inject
MediaWikiApi mediaWikiApi;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
/**
* This method helps in the creation Achievement screen and
* dynamically set the size of imageView
*
* @param savedInstanceState Data bundle
*/
@Override
@SuppressLint("StringFormatInvalid")
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_achievements);
ButterKnife.bind(this);
/**
* DisplayMetrics used to fetch the size of the screen
*/
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int height = displayMetrics.heightPixels;
int width = displayMetrics.widthPixels;
/**
* Used for the setting the size of imageView at runtime
*/
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams)
imageView.getLayoutParams();
params.height = (int) (height * BADGE_IMAGE_HEIGHT_RATIO);
params.width = (int) (width * BADGE_IMAGE_WIDTH_RATIO);
imageView.setImageResource(R.drawable.badge);
imageView.requestLayout();
setSupportActionBar(toolbar);
progressBar.setVisibility(View.VISIBLE);
hideLayouts();
setAchievements();
setUploadCount();
setRevertCount();
initDrawer();
}
/**
* to invoke the AlertDialog on clicking info button
*/
@OnClick(R.id.achievement_info)
public void showInfoDialog(){
launchAlert(getResources().getString(R.string.Achievements)
,getResources().getString(R.string.achievements_info_message));
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_about, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
int id = item.getItemId();
if (id == R.id.share_app_icon) {
View rootView = getWindow().getDecorView().findViewById(android.R.id.content);
Bitmap screenShot = Utils.getScreenShot(rootView);
showAlert(screenShot);
}
return super.onOptionsItemSelected(item);
}
/**
* To take bitmap and store it temporary storage and share it
*
* @param bitmap
*/
void shareScreen(Bitmap bitmap) {
try {
File file = new File(this.getExternalCacheDir(), "screen.png");
FileOutputStream fOut = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);
fOut.flush();
fOut.close();
file.setReadable(true, false);
final Intent intent = new Intent(android.content.Intent.ACTION_SEND);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file));
intent.setType("image/png");
startActivity(Intent.createChooser(intent, "Share image via"));
} catch (IOException e) {
//Do Nothing
}
}
/**
* To call the API to get results in form Single<JSONObject>
* which then calls parseJson when results are fetched
*/
private void setAchievements() {
if(checkAccount()) {
compositeDisposable.add(mediaWikiApi
.getAchievements(sessionManager.getCurrentAccount().name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
jsonObject -> parseJson(jsonObject),
t -> Timber.e(t, "Fetching achievements statisticss failed")
));
}
}
/**
* To call the API to get reverts count in form of JSONObject
*
*/
private void setRevertCount(){
if(checkAccount()) {
compositeDisposable.add(mediaWikiApi
.getRevertCount(sessionManager.getCurrentAccount().name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
object -> parseJsonRevertCount(object),
t -> Timber.e(t, "Fetching revert count failed")
));
}
}
/**
* used to set number of deleted images
* @param object
*/
private void parseJsonRevertCount(JSONObject object){
try {
achievements.setRevertCount(object.getInt("deletedUploads"));
} catch (JSONException e) {
Timber.d( e, e.getMessage());
}
isRevertFetched = true;
hideProgressBar();
}
/**
* used to the count of images uploaded by user
*/
private void setUploadCount() {
if(checkAccount()) {
compositeDisposable.add(mediaWikiApi
.getUploadCount(sessionManager.getCurrentAccount().name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
uploadCount -> setAchievementsUploadCount(uploadCount),
t -> Timber.e(t, "Fetching upload count failed")
));
}
}
/**
* used to set achievements upload count and call hideProgressbar
* @param uploadCount
*/
private void setAchievementsUploadCount(int uploadCount){
achievements.setImagesUploaded(uploadCount);
isUploadFetched = true;
hideProgressBar();
}
/**
* used to the uploaded images progressbar
* @param uploadCount
*/
private void setUploadProgress(int uploadCount){
imagesUploadedProgressbar.setProgress
(100*uploadCount/levelInfo.getMaxUploadCount());
imagesUploadedProgressbar.setProgressTextFormatPattern
(uploadCount +"/" + levelInfo.getMaxUploadCount() );
}
/**
* used to set the non revert image percentage
* @param notRevertPercentage
*/
private void setImageRevertPercentage(int notRevertPercentage){
imageRevertsProgressbar.setProgress(notRevertPercentage);
String revertPercentage = Integer.toString(notRevertPercentage);
imageRevertsProgressbar.setProgressTextFormatPattern(revertPercentage + "%%");
imagesRevertLimitText.setText(getResources().getString(R.string.achievements_revert_limit_message)+ levelInfo.getMinNonRevertPercentage() + "%");
}
/**
* used to parse the JSONObject containing results
* @param object
*/
private void parseJson(JSONObject object) {
try {
achievements.setUniqueUsedImages(object.getInt("uniqueUsedImages"));
achievements.setArticlesUsingImages(object.getInt("articlesUsingImages"));
achievements.setThanksReceived(object.getInt("thanksReceived"));
achievements.setImagesEditedBySomeoneElse(object.getInt("imagesEditedBySomeoneElse"));
JSONObject featuredImages = object.getJSONObject("featuredImages");
achievements.setFeaturedImages
(featuredImages.getInt("Quality_images") +
featuredImages.getInt("Featured_pictures_on_Wikimedia_Commons"));
} catch (JSONException e) {
e.printStackTrace();
}
isStatisticsFetched = true;
hideProgressBar();
}
/**
* Used the inflate the fetched statistics of the images uploaded by user
* and assign badge and level
* @param achievements
*/
private void inflateAchievements(Achievements achievements ){
thanksReceived.setText(Integer.toString(achievements.getThanksReceived()));
imagesUsedByWikiProgessbar.setProgress
(100*achievements.getUniqueUsedImages()/levelInfo.getMaxUniqueImages() );
imagesUsedByWikiProgessbar.setProgressTextFormatPattern
(achievements.getUniqueUsedImages() + "/" + levelInfo.getMaxUniqueImages());
imagesFeatured.setText(Integer.toString(achievements.getFeaturedImages()));
String levelUpInfoString = getString(R.string.level);
levelUpInfoString += " " + Integer.toString(levelInfo.getLevelNumber());
levelNumber.setText(levelUpInfoString);
final ContextThemeWrapper wrapper = new ContextThemeWrapper(this, levelInfo.getLevelStyle());
Drawable drawable = ResourcesCompat.getDrawable(getResources(), R.drawable.badge, wrapper.getTheme());
Bitmap bitmap = BitmapUtils.drawableToBitmap(drawable);
BitmapDrawable bitmapImage = BitmapUtils.writeOnDrawable(bitmap, Integer.toString(levelInfo.getLevelNumber()),this);
imageView.setImageDrawable(bitmapImage);
}
/**
* Creates a way to change current activity to AchievementActivity
* @param context
*/
public static void startYourself(Context context) {
Intent intent = new Intent(context, AchievementsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
context.startActivity(intent);
}
/**
* to hide progressbar
*/
private void hideProgressBar() {
if (progressBar != null && isUploadFetched && isStatisticsFetched && isRevertFetched) {
levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(),
achievements.getUniqueUsedImages(),
achievements.getNotRevertPercentage());
inflateAchievements(achievements);
setUploadProgress(achievements.getImagesUploaded());
setImageRevertPercentage(achievements.getNotRevertPercentage());
progressBar.setVisibility(View.GONE);
layoutImageReverts.setVisibility(View.VISIBLE);
layoutImageUploaded.setVisibility(View.VISIBLE);
layoutImageUsedByWiki.setVisibility(View.VISIBLE);
layoutStatistics.setVisibility(View.VISIBLE);
imageView.setVisibility(View.VISIBLE);
levelNumber.setVisibility(View.VISIBLE);
}
}
/**
* used to hide the layouts while fetching results from api
*/
private void hideLayouts(){
layoutImageUsedByWiki.setVisibility(View.INVISIBLE);
layoutImageUploaded.setVisibility(View.INVISIBLE);
layoutImageReverts.setVisibility(View.INVISIBLE);
layoutStatistics.setVisibility(View.INVISIBLE);
imageView.setVisibility(View.INVISIBLE);
levelNumber.setVisibility(View.INVISIBLE);
}
/**
* It display the alertDialog with Image of screenshot
* @param screenshot
*/
public void showAlert(Bitmap screenshot){
AlertDialog.Builder alertadd = new AlertDialog.Builder(AchievementsActivity.this);
LayoutInflater factory = LayoutInflater.from(AchievementsActivity.this);
final View view = factory.inflate(R.layout.image_alert_layout, null);
ImageView screenShotImage = (ImageView) view.findViewById(R.id.alert_image);
screenShotImage.setImageBitmap(screenshot);
TextView shareMessage = (TextView) view.findViewById(R.id.alert_text);
shareMessage.setText(R.string.achievements_share_message);
alertadd.setView(view);
alertadd.setPositiveButton("Proceed", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
shareScreen(screenshot);
}
});
alertadd.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
alertadd.show();
}
@OnClick(R.id.images_upload_info)
public void showUploadInfo(){
launchAlert(getResources().getString(R.string.images_uploaded)
,getResources().getString(R.string.images_uploaded_explanation));
}
@OnClick(R.id.images_reverted_info)
public void showRevertedInfo(){
launchAlert(getResources().getString(R.string.image_reverts)
,getResources().getString(R.string.images_reverted_explanation));
}
@OnClick(R.id.images_used_by_wiki_info)
public void showUsedByWikiInfo(){
launchAlert(getResources().getString(R.string.images_used_by_wiki)
,getResources().getString(R.string.images_used_explanation));
}
/**
* takes title and message as input to display alerts
* @param title
* @param message
*/
private void launchAlert(String title, String message){
new AlertDialog.Builder(AchievementsActivity.this)
.setTitle(title)
.setMessage(message)
.setCancelable(true)
.setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
.create()
.show();
}
/**
* check to ensure that user is logged in
* @return
*/
private boolean checkAccount(){
Account currentAccount = sessionManager.getCurrentAccount();
if(currentAccount == null) {
Timber.d("Current account is null");
ViewUtil.showLongToast(this, getResources().getString(R.string.user_not_logged_in));
sessionManager.forceLogin(this);
return false;
}
return true;
}
}

View file

@ -0,0 +1,54 @@
package fr.free.nrw.commons.achievements;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
public class BitmapUtils {
/**
* write level Number on the badge
* @param bm
* @param text
* @return
*/
public static BitmapDrawable writeOnDrawable(Bitmap bm, String text, Context context){
Bitmap.Config config = bm.getConfig();
if(config == null){
config = Bitmap.Config.ARGB_8888;
}
Bitmap bitmap = Bitmap.createBitmap(bm.getWidth(),bm.getHeight(),config);
Canvas canvas = new Canvas(bitmap);
canvas.drawBitmap(bm, 0, 0, null);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.FILL);
paint.setColor(Color.WHITE);
paint.setTextSize(Math.round(canvas.getHeight()/2));
paint.setTextAlign(Paint.Align.CENTER);
Rect rectText = new Rect();
paint.getTextBounds(text,0, text.length(),rectText);
canvas.drawText(text, Math.round(canvas.getWidth()/2),Math.round(canvas.getHeight()/1.35), paint);
return new BitmapDrawable(context.getResources(), bitmap);
}
/**
* Convert Drawable to bitmap
* @param drawable
* @return
*/
public static Bitmap drawableToBitmap (Drawable drawable) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable)drawable).getBitmap();
}
Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
}

View file

@ -0,0 +1,85 @@
package fr.free.nrw.commons.achievements;
import android.util.Log;
import fr.free.nrw.commons.R;
/**
* calculates the level of the user
*/
public class LevelController {
public LevelInfo level;
public enum LevelInfo{
LEVEL_1(1, R.style.LevelOne, 5, 20, 85),
LEVEL_2(2, R.style.LevelTwo, 10, 30, 86),
LEVEL_3(3, R.style.LevelThree, 15,40, 87),
LEVEL_4(4, R.style.LevelFour,20,50, 88),
LEVEL_5(5, R.style.LevelFive, 25, 60, 89),
LEVEL_6(6,R.style.LevelOne,30,70, 90),
LEVEL_7(7, R.style.LevelTwo, 40, 80, 90),
LEVEL_8(8, R.style.LevelThree, 45, 90, 90),
LEVEL_9(9, R.style.LevelFour, 50, 100, 90),
LEVEL_10(10, R.style.LevelFive, 55, 110, 90),
LEVEL_11(11,R.style.LevelOne, 60, 120, 90),
LEVEL_12(12,R.style.LevelTwo,65 , 130, 90),
LEVEL_13(13,R.style.LevelThree, 70, 140, 90),
LEVEL_14(14,R.style.LevelFour, 75 , 150, 90),
LEVEL_15(15,R.style.LevelFive, 80, 160, 90);
private int levelNumber;
private int levelStyle;
private int maxUniqueImages;
private int maxUploadCount;
private int minNonRevertPercentage;
LevelInfo(int levelNumber,
int levelStyle,
int maxUniqueImages,
int maxUploadCount,
int minNonRevertPercentage) {
this.levelNumber = levelNumber;
this.levelStyle = levelStyle;
this.maxUniqueImages = maxUniqueImages;
this.maxUploadCount = maxUploadCount;
this.minNonRevertPercentage = minNonRevertPercentage;
}
public static LevelInfo from(int imagesUploaded,
int uniqueImagesUsed,
int nonRevertRate) {
LevelInfo level = LEVEL_15;
for (LevelInfo levelInfo : LevelInfo.values()) {
if (imagesUploaded < levelInfo.maxUploadCount
|| uniqueImagesUsed < levelInfo.maxUniqueImages
|| nonRevertRate < levelInfo.minNonRevertPercentage ) {
level = levelInfo;
return level;
}
}
return level;
}
public int getLevelStyle() {
return levelStyle;
}
public int getLevelNumber() {
return levelNumber;
}
public int getMaxUniqueImages() {
return maxUniqueImages;
}
public int getMaxUploadCount() {
return maxUploadCount;
}
public int getMinNonRevertPercentage(){
return minNonRevertPercentage;
}
}
}

View file

@ -4,8 +4,13 @@ import android.os.Bundle;
import javax.inject.Inject;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
@ -34,6 +39,8 @@ public abstract class AuthenticatedActivity extends NavigationBaseActivity {
if (savedInstanceState != null) {
authCookie = savedInstanceState.getString(AUTH_COOKIE);
}
showBlockStatus();
}
@Override
@ -45,4 +52,20 @@ public abstract class AuthenticatedActivity extends NavigationBaseActivity {
protected abstract void onAuthCookieAcquired(String authCookie);
protected abstract void onAuthFailure();
/**
* Makes API call to check if user is blocked from Commons. If the user is blocked, a snackbar
* is created to notify the user
*/
protected void showBlockStatus()
{
Observable.fromCallable(() -> mediaWikiApi.isUserBlockedFromCommons())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.filter(result -> result)
.subscribe(result -> {
ViewUtil.showSnackbar(findViewById(android.R.id.content), R.string.block_notification);
}
);
}
}

View file

@ -5,6 +5,7 @@ import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
@ -41,10 +42,10 @@ 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.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.ui.widget.HtmlTextView;
import fr.free.nrw.commons.utils.ViewUtil;
@ -62,6 +63,7 @@ import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
public class LoginActivity extends AccountAuthenticatorActivity {
public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username";
private static final String FEATURED_IMAGES_CATEGORY = "Category:Featured_pictures_on_Wikimedia_Commons";
@Inject MediaWikiApi mwApi;
@Inject AccountUtil accountUtil;
@ -146,9 +148,13 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
}
/**
* This function is called when user skips the login.
* It redirects the user to Explore Activity.
*/
private void skipLogin() {
prefs.edit().putBoolean("login_skipped", true).apply();
NavigationBaseActivity.startActivityWithFlags(this, NearbyActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP);
CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_explore), FEATURED_IMAGES_CATEGORY);
finish();
}
@ -180,6 +186,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
&& sessionManager.isUserLoggedIn()
&& sessionManager.getCachedAuthCookie() != null) {
prefs.edit().putBoolean("login_skipped", false).apply();
sessionManager.revalidateAuthToken();
startMainActivity();
}
@ -294,11 +301,11 @@ public class LoginActivity extends AccountAuthenticatorActivity {
showMessageAndCancelDialog(R.string.login_failed_network);
} 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(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(Locale.getDefault()).contains("throttle".toLowerCase())) {
// Matches unknown throttle error codes
@ -463,4 +470,9 @@ public class LoginActivity extends AccountAuthenticatorActivity {
loginButton.setEnabled(enabled);
}
}
public static void startYourself(Context context) {
Intent intent = new Intent(context, LoginActivity.class);
context.startActivity(intent);
}
}

View file

@ -5,6 +5,8 @@ import android.accounts.AccountManager;
import android.content.Context;
import android.content.SharedPreferences;
import javax.annotation.Nullable;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import io.reactivex.Completable;
import io.reactivex.Observable;
@ -31,6 +33,7 @@ public class SessionManager {
/**
* @return Account|null
*/
@Nullable
public Account getCurrentAccount() {
if (currentAccount == null) {
AccountManager accountManager = AccountManager.get(context);
@ -81,6 +84,12 @@ public class SessionManager {
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);

View file

@ -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<List<String>> quadTree;
private double x, y;
private QuadTree<List<String>> 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<String> 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;

View file

@ -39,7 +39,7 @@ 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.GpsCategoryModel;
import fr.free.nrw.commons.utils.StringSortingUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
@ -73,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<CategoryItem> categoriesAdapter;
private OnCategoriesSaveHandler onCategoriesSaveHandler;
@ -253,7 +254,6 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
}
private Observable<CategoryItem> defaultCategories() {
Observable<CategoryItem> directCat = directCategories();
if (hasDirectCategories) {
Timber.d("Image has direct Cat");
@ -287,9 +287,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
}
private Observable<CategoryItem> gpsCategories() {
return Observable.fromIterable(
MwVolleyApi.GpsCatExists.getGpsCatExists()
? MwVolleyApi.getGpsCat() : new ArrayList<>())
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
.map(name -> new CategoryItem(name, false));
}

View file

@ -0,0 +1,253 @@
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.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.view.ViewPager;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.FrameLayout;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.explore.ViewPagerAdapter;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import static android.widget.Toast.LENGTH_SHORT;
/**
* This activity displays details of a particular category
* Its generic and simply takes the name of category name in its start intent to load all images, subcategories in
* a particular category on wikimedia commons.
*/
public class CategoryDetailsActivity extends NavigationBaseActivity
implements MediaDetailPagerFragment.MediaDetailProvider,
AdapterView.OnItemClickListener{
private FragmentManager supportFragmentManager;
private CategoryImagesListFragment categoryImagesListFragment;
private MediaDetailPagerFragment mediaDetails;
private String categoryName;
@BindView(R.id.mediaContainer) FrameLayout mediaContainer;
@BindView(R.id.tabLayout) TabLayout tabLayout;
@BindView(R.id.viewPager) ViewPager viewPager;
ViewPagerAdapter viewPagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_category_details);
ButterKnife.bind(this);
supportFragmentManager = getSupportFragmentManager();
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(viewPagerAdapter);
viewPager.setOffscreenPageLimit(2);
tabLayout.setupWithViewPager(viewPager);
setTabs();
setPageTitle();
initDrawer();
forceInitBackButton();
}
/**
* This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab,
* Set the fragments according to the tab selected in the viewPager.
*/
private void setTabs() {
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
categoryImagesListFragment = new CategoryImagesListFragment();
SubCategoryListFragment subCategoryListFragment = new SubCategoryListFragment();
SubCategoryListFragment parentCategoryListFragment = new SubCategoryListFragment();
categoryName = getIntent().getStringExtra("categoryName");
if (getIntent() != null && categoryName != null) {
Bundle arguments = new Bundle();
arguments.putString("categoryName", categoryName);
arguments.putBoolean("isParentCategory", false);
categoryImagesListFragment.setArguments(arguments);
subCategoryListFragment.setArguments(arguments);
Bundle parentCategoryArguments = new Bundle();
parentCategoryArguments.putString("categoryName", categoryName);
parentCategoryArguments.putBoolean("isParentCategory", true);
parentCategoryListFragment.setArguments(parentCategoryArguments);
}
fragmentList.add(categoryImagesListFragment);
titleList.add("MEDIA");
fragmentList.add(subCategoryListFragment);
titleList.add("SUBCATEGORIES");
fragmentList.add(parentCategoryListFragment);
titleList.add("PARENT CATEGORIES");
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPagerAdapter.notifyDataSetChanged();
}
/**
* Gets the passed categoryName from the intents and displays it as the page title
*/
private void setPageTitle() {
if (getIntent() != null && getIntent().getStringExtra("categoryName") != null) {
setTitle(getIntent().getStringExtra("categoryName"));
}
}
/**
* This method is called onClick of media inside category details (CategoryImageListFragment).
*/
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
tabLayout.setVisibility(View.GONE);
viewPager.setVisibility(View.GONE);
mediaContainer.setVisibility(View.VISIBLE);
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.mediaContainer, mediaDetails)
.addToBackStack(null)
.commit();
supportFragmentManager.executePendingTransactions();
}
mediaDetails.showImage(i);
forceInitBackButton();
}
/**
* Consumers should be simply using this method to use this activity.
* @param context A Context of the application package implementing this class.
* @param categoryName Name of the category for displaying its details
*/
public static void startYourself(Context context, String categoryName) {
Intent intent = new Intent(context, CategoryDetailsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
intent.putExtra("categoryName", categoryName);
context.startActivity(intent);
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
* @param i It is the index of which media object is to be returned which is same as
* current index of viewPager.
* @return Media Object
*/
@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);
}
}
/**
* This method is called on from getCount of MediaDetailPagerFragment
* The viewpager will contain same number of media items as that of media elements in adapter.
* @return Total Media count in the adapter
*/
@Override
public int getTotalMediaCount() {
if (categoryImagesListFragment.getAdapter() == null) {
return 0;
}
return categoryImagesListFragment.getAdapter().getCount();
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void notifyDatasetChanged() {
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void registerDataSetObserver(DataSetObserver observer) {
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
}
/**
* This method inflates the menu in the toolbar
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.fragment_category_detail, menu);
return super.onCreateOptionsMenu(menu);
}
/**
* This method handles the logic on ItemSelect in toolbar menu
* Currently only 1 choice is available to open category details page in browser
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.menu_browser_current_category:
Intent viewIntent = new Intent();
viewIntent.setAction(Intent.ACTION_VIEW);
viewIntent.setData(new PageTitle(categoryName).getCanonicalUri());
//check if web browser available
if (viewIntent.resolveActivity(this.getPackageManager()) != null) {
startActivity(viewIntent);
} else {
Toast toast = Toast.makeText(this, getString(R.string.no_web_browser), LENGTH_SHORT);
toast.show();
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* This method is called on backPressed of anyFragment in the activity.
* If condition is called when mediaDetailFragment is opened.
*/
@Override
public void onBackPressed() {
if (supportFragmentManager.getBackStackEntryCount() == 1){
// back to search so show search toolbar and hide navigation toolbar
tabLayout.setVisibility(View.VISIBLE);
viewPager.setVisibility(View.VISIBLE);
mediaContainer.setVisibility(View.GONE);
}
super.onBackPressed();
}
}

View file

@ -8,6 +8,7 @@ import org.w3c.dom.NodeList;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@ -27,12 +28,30 @@ public class CategoryImageUtils {
List<Media> categoryImages = new ArrayList<>();
for (int i = 0; i < childNodes.getLength(); i++) {
Node node = childNodes.item(i);
categoryImages.add(getMediaFromPage(node));
if (getMediaFromPage(node).getFilename().substring(0,5).equals("File:")){
categoryImages.add(getMediaFromPage(node));
}
}
return categoryImages;
}
/**
* The method iterates over the child nodes to return a list of Subcategory name
* sorted alphabetically
* @param childNodes
* @return
*/
public static List<String> getSubCategoryList(NodeList childNodes) {
List<String> subCategories = new ArrayList<>();
for (int i = 0; i < childNodes.getLength(); i++) {
Node node = childNodes.item(i);
subCategories.add(getMediaFromPage(node).getFilename());
}
Collections.sort(subCategories);
return subCategories;
}
/**
* Creates a new Media object from the XML response as received by the API
* @param node

View file

@ -6,6 +6,9 @@ import android.database.DataSetObserver;
import android.os.Bundle;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
@ -13,8 +16,9 @@ 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.explore.SearchActivity;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import timber.log.Timber;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
/**
* This activity displays pictures of a particular category
@ -44,6 +48,16 @@ public class CategoryImagesActivity
}
/**
* This method is called on backPressed of anyFragment in the activity.
* We are changing the icon here from back to hamburger icon.
*/
@Override
public void onBackPressed() {
initDrawer();
super.onBackPressed();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -55,11 +69,6 @@ public class CategoryImagesActivity
supportFragmentManager = getSupportFragmentManager();
setCategoryImagesFragment();
supportFragmentManager.addOnBackStackChangedListener(this);
if (savedInstanceState != null) {
mediaDetails = (MediaDetailPagerFragment) supportFragmentManager
.findFragmentById(R.id.fragmentContainer);
}
requestAuthToken();
initDrawer();
setPageTitle();
@ -95,6 +104,9 @@ public class CategoryImagesActivity
public void onBackStackChanged() {
}
/**
* This method is called onClick of media inside category details (CategoryImageListFragment).
*/
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
if (mediaDetails == null || !mediaDetails.isVisible()) {
@ -103,17 +115,34 @@ public class CategoryImagesActivity
FragmentManager supportFragmentManager = getSupportFragmentManager();
supportFragmentManager
.beginTransaction()
.replace(R.id.fragmentContainer, mediaDetails)
.hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount()))
.add(R.id.fragmentContainer, mediaDetails)
.addToBackStack(null)
.commit();
supportFragmentManager.executePendingTransactions();
// Reason for using hide, add instead of replace is to maintain scroll position after
// coming back to the search activity. See https://github.com/commons-app/apps-android-commons/issues/1631
// https://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack/19022550#19022550 supportFragmentManager.executePendingTransactions();
}
mediaDetails.showImage(i);
forceInitBackButton();
}
/**
* This method is called on backPressed when mediaDetailFragment is opened in the activity.
*/
@Override
protected void onResume() {
if (supportFragmentManager.getBackStackEntryCount()==1){
//FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time.
//FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894
onBackPressed();
}
super.onResume();
}
/**
* Consumers should be simply using this method to use this activity.
* @param context
* @param context A Context of the application package implementing this class.
* @param title Page title
* @param categoryName Name of the category for displaying its images
*/
@ -125,6 +154,12 @@ public class CategoryImagesActivity
context.startActivity(intent);
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
* @param i It is the index of which media object is to be returned which is same as
* current index of viewPager.
* @return Media Object
*/
@Override
public Media getMediaAtPosition(int i) {
if (categoryImagesListFragment.getAdapter() == null) {
@ -135,6 +170,11 @@ public class CategoryImagesActivity
}
}
/**
* This method is called on from getCount of MediaDetailPagerFragment
* The viewpager will contain same number of media items as that of media elements in adapter.
* @return Total Media count in the adapter
*/
@Override
public int getTotalMediaCount() {
if (categoryImagesListFragment.getAdapter() == null) {
@ -143,18 +183,57 @@ public class CategoryImagesActivity
return categoryImagesListFragment.getAdapter().getCount();
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void notifyDatasetChanged() {
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void registerDataSetObserver(DataSetObserver observer) {
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
}
/**
* This method inflates the menu in the toolbar
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_search, menu);
return super.onCreateOptionsMenu(menu);
}
/**
* This method handles the logic on ItemSelect in toolbar menu
* Currently only 1 choice is available to open search page of the app
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.action_search:
NavigationBaseActivity.startActivityWithFlags(this, SearchActivity.class);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
}

View file

@ -12,7 +12,9 @@ import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ListAdapter;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.util.List;
import java.util.concurrent.TimeUnit;
@ -48,9 +50,9 @@ public class CategoryImagesListFragment extends DaggerFragment {
TextView statusTextView;
@BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar;
@BindView(R.id.categoryImagesList) GridView gridView;
@BindView(R.id.parentLayout) RelativeLayout parentLayout;
private boolean hasMoreImages = true;
private boolean isLoading;
private boolean isLoading = true;
private String categoryName = null;
@Inject CategoryImageController controller;
@ -123,7 +125,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
statusTextView.setVisibility(VISIBLE);
statusTextView.setText(getString(R.string.no_internet));
} else {
ViewUtil.showSnackbar(gridView, R.string.no_internet);
ViewUtil.showSnackbar(parentLayout, R.string.no_internet);
}
}
@ -132,15 +134,20 @@ public class CategoryImagesListFragment extends DaggerFragment {
* @param throwable
*/
private void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading featured images");
initErrorView();
Timber.e(throwable, "Error occurred while loading images inside a category");
try{
ViewUtil.showSnackbar(parentLayout, R.string.error_loading_images);
initErrorView();
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 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);
@ -152,7 +159,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
/**
* Initializes the adapter with a list of Media objects
* @param mediaList
* @param mediaList List of new Media to be displayed
*/
private void setAdapter(List<Media> mediaList) {
gridAdapter = new GridViewAdapter(this.getContext(), R.layout.layout_category_images, mediaList);
@ -176,6 +183,9 @@ public class CategoryImagesListFragment extends DaggerFragment {
isLoading = true;
fetchMoreImages();
}
if (!hasMoreImages){
progressBar.setVisibility(GONE);
}
}
});
}
@ -201,7 +211,7 @@ public class CategoryImagesListFragment extends DaggerFragment {
/**
* Handles the success scenario
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
* @param collection
* @param collection List of new Media to be displayed
*/
private void handleSuccess(List<Media> collection) {
if(collection == null || collection.isEmpty()) {
@ -213,6 +223,10 @@ public class CategoryImagesListFragment extends DaggerFragment {
if(gridAdapter == null) {
setAdapter(collection);
} else {
if (gridAdapter.containsAll(collection)) {
hasMoreImages = false;
return;
}
gridAdapter.addItems(collection);
}
@ -221,7 +235,13 @@ public class CategoryImagesListFragment extends DaggerFragment {
statusTextView.setVisibility(GONE);
}
/**
* It return an instance of gridView adapter which helps in extracting media details
* used by the gridView
* @return GridView Adapter
*/
public ListAdapter getAdapter() {
return gridView.getAdapter();
}
}

View file

@ -42,6 +42,18 @@ public class GridViewAdapter extends ArrayAdapter {
notifyDataSetChanged();
}
/**
* Check the first item in the new list with old list and returns true if they are same
* Its triggered on successful response of the fetch images API.
* @param images
*/
public boolean containsAll(List<Media> images){
if (data == null) {
data = new ArrayList<>();
}
return images.get(0).getFilename().equals(data.get(0).getFilename());
}
@Override
public boolean isEmpty() {
return data == null || data.isEmpty();
@ -66,7 +78,7 @@ public class GridViewAdapter extends ArrayAdapter {
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());
fileName.setText(item.getDisplayTitle());
setAuthorView(item, author);
imageView.setMedia(item);
return convertView;

View file

@ -0,0 +1,167 @@
package fr.free.nrw.commons.category;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.categories.SearchCategoriesAdapterFactory;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
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 the category search screen.
*/
public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
private static int TIMEOUT_SECONDS = 15;
@BindView(R.id.imagesListBox)
RecyclerView categoriesRecyclerView;
@BindView(R.id.imageSearchInProgress)
ProgressBar progressBar;
@BindView(R.id.imagesNotFound)
TextView categoriesNotFoundView;
private String categoryName = null;
@Inject MediaWikiApi mwApi;
private RVRendererAdapter<String> categoriesAdapter;
private boolean isParentCategory = true;
private final SearchCategoriesAdapterFactory adapterFactory = new SearchCategoriesAdapterFactory(item -> {
// Open SubCategory Details page
Intent intent = new Intent(getContext(), CategoryDetailsActivity.class);
intent.putExtra("categoryName", item);
getContext().startActivity(intent);
});
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
ButterKnife.bind(this, rootView);
categoryName = getArguments().getString("categoryName");
isParentCategory = getArguments().getBoolean("isParentCategory");
initSubCategoryList();
if(getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
}
else{
categoriesRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
}
ArrayList<String> items = new ArrayList<>();
categoriesAdapter = adapterFactory.create(items);
categoriesRecyclerView.setAdapter(categoriesAdapter);
return rootView;
}
/**
* Checks for internet connection and then initializes the recycler view with 25 categories of the searched query
* Clearing categoryAdapter every time new keyword is searched so that user can see only new results
*/
public void initSubCategoryList() {
categoriesNotFoundView.setVisibility(GONE);
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
progressBar.setVisibility(View.VISIBLE);
if (!isParentCategory){
Observable.fromCallable(() -> mwApi.getSubCategoryList(categoryName))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(this::handleSuccess, this::handleError);
}else {
Observable.fromCallable(() -> mwApi.getParentCategoryList(categoryName))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(this::handleSuccess, this::handleError);
}
}
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
* @param subCategoryList
*/
private void handleSuccess(List<String> subCategoryList) {
if(subCategoryList == null || subCategoryList.isEmpty()) {
initEmptyView();
}
else {
progressBar.setVisibility(View.GONE);
categoriesAdapter.addAll(subCategoryList);
categoriesAdapter.notifyDataSetChanged();
}
}
/**
* Logs and handles API error scenario
* @param throwable
*/
private void handleError(Throwable throwable) {
if (!isParentCategory){
Timber.e(throwable, "Error occurred while loading queried subcategories");
ViewUtil.showSnackbar(categoriesRecyclerView,R.string.error_loading_categories);
}else {
Timber.e(throwable, "Error occurred while loading queried parentcategories");
ViewUtil.showSnackbar(categoriesRecyclerView,R.string.error_loading_categories);
}
}
/**
* Handles the UI updates for a empty results scenario
*/
private void initEmptyView() {
progressBar.setVisibility(GONE);
categoriesNotFoundView.setVisibility(VISIBLE);
if (!isParentCategory){
categoriesNotFoundView.setText(getString(R.string.no_subcategory_found));
}else {
categoriesNotFoundView.setText(getString(R.string.no_parentcategory_found));
}
}
/**
* Handles the UI updates for no internet scenario
*/
private void handleNoInternet() {
progressBar.setVisibility(GONE);
ViewUtil.showSnackbar(categoriesRecyclerView, R.string.no_internet);
}
}

View file

@ -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;
}
}

View file

@ -24,6 +24,7 @@ import static android.content.Intent.EXTRA_STREAM;
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA;
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY;
import static fr.free.nrw.commons.upload.UploadService.EXTRA_SOURCE;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
public class ContributionController {
@ -90,7 +91,8 @@ 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) {
Timber.d("Is direct upload %s and the Wikidata entity ID is %s", isDirectUpload, wikiDataEntityId);
FragmentActivity activity = fragment.getActivity();
Timber.d("handleImagePicked() called with onActivityResult()");
Intent shareIntent = new Intent(activity, ShareActivity.class);
@ -102,9 +104,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 +112,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 +119,10 @@ public class ContributionController {
}
Timber.i("Image selected");
try {
shareIntent.putExtra("isDirectUpload", isDirectUpload);
if (wikiDataEntityId != null && !wikiDataEntityId.equals("")) {
shareIntent.putExtra(WIKIDATA_ENTITY_ID_PREF, wikiDataEntityId);
}
activity.startActivity(shareIntent);
} catch (SecurityException e) {
Timber.e(e, "Security Exception");

View file

@ -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;

View file

@ -276,17 +276,25 @@ public class ContributionsActivity
.getUploadCount(sessionManager.getCurrentAccount().name)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
uploadCount -> getSupportActionBar().setSubtitle(getResources()
.getQuantityString(R.plurals.contributions_subtitle,
uploadCount, uploadCount)),
.subscribe(this::displayUploadCount,
t -> Timber.e(t, "Fetching upload count failed")
));
}
public void betaSetUploadCount(int betaUploadCount){
private void displayUploadCount(Integer uploadCount) {
if (isFinishing()
|| getSupportActionBar() == null
|| getResources() == null) {
return;
}
getSupportActionBar().setSubtitle(getResources()
.getQuantityString(R.plurals.contributions_subtitle, betaUploadCount, betaUploadCount));
.getQuantityString(R.plurals.contributions_subtitle,
uploadCount, uploadCount));
}
public void betaSetUploadCount(int betaUploadCount) {
displayUploadCount(betaUploadCount);
}

View file

@ -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);

View file

@ -6,12 +6,13 @@ import android.database.sqlite.SQLiteOpenHelper;
import fr.free.nrw.commons.category.CategoryDao;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
public class DBOpenHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "commons.db";
private static final int DATABASE_VERSION = 6;
private static final int DATABASE_VERSION = 7;
/**
* Do not use directly - @Inject an instance where it's needed and let
@ -26,6 +27,7 @@ public class DBOpenHelper extends SQLiteOpenHelper {
ContributionDao.Table.onCreate(sqLiteDatabase);
ModifierSequenceDao.Table.onCreate(sqLiteDatabase);
CategoryDao.Table.onCreate(sqLiteDatabase);
RecentSearchesDao.Table.onCreate(sqLiteDatabase);
}
@Override
@ -33,5 +35,6 @@ public class DBOpenHelper extends SQLiteOpenHelper {
ContributionDao.Table.onUpdate(sqLiteDatabase, from, to);
ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to);
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
RecentSearchesDao.Table.onUpdate(sqLiteDatabase, from, to);
}
}

View file

@ -4,10 +4,14 @@ import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.AboutActivity;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.achievements.AchievementsActivity;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
@ -50,4 +54,14 @@ public abstract class ActivityBuilderModule {
@ContributesAndroidInjector
abstract CategoryImagesActivity bindFeaturedImagesActivity();
@ContributesAndroidInjector
abstract SearchActivity bindSearchActivity();
@ContributesAndroidInjector
abstract CategoryDetailsActivity bindCategoryDetailsActivity();
@ContributesAndroidInjector
abstract AchievementsActivity bindAchievementsActivity();
}

View file

@ -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<Application
void inject(PlaceRenderer placeRenderer);
void inject(FileProcessor fileProcessor);
@Component.Builder
@SuppressWarnings({"WeakerAccess", "unused"})
interface Builder {

View file

@ -6,34 +6,31 @@ import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v4.util.LruCache;
import com.google.gson.Gson;
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.CommonsApplication;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.nearby.NearbyPlaces;
import fr.free.nrw.commons.upload.UploadController;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
import static android.content.Context.MODE_PRIVATE;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.CONTRIBUTION_AUTHORITY;
import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.RECENT_SEARCH_AUTHORITY;
import static fr.free.nrw.commons.modifications.ModificationsContentProvider.MODIFICATIONS_AUTHORITY;
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
public class CommonsApplicationModule {
public static final String CATEGORY_AUTHORITY = "fr.free.nrw.commons.categories.contentprovider";
public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024;
private Context applicationContext;
@ -57,6 +54,18 @@ public class CommonsApplicationModule {
return context.getContentResolver().acquireContentProviderClient(CATEGORY_AUTHORITY);
}
/**
* This method is used to provide instance of RecentSearchContentProviderClient
* which provides content of Recent Searches from database
* @param context
* @return returns RecentSearchContentProviderClient
*/
@Provides
@Named("recentsearch")
public ContentProviderClient provideRecentSearchContentProviderClient(Context context) {
return context.getContentResolver().acquireContentProviderClient(RECENT_SEARCH_AUTHORITY);
}
@Provides
@Named("contribution")
public ContentProviderClient provideContributionContentProviderClient(Context context) {
@ -117,37 +126,12 @@ public class CommonsApplicationModule {
return new SessionManager(context, mediaWikiApi, sharedPreferences);
}
@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, defaultPreferences, categoryPrefs, gson);
}
@Provides
@Singleton
public LocationServiceManager provideLocationServiceManager(Context context) {
return new LocationServiceManager(context);
}
/**
* 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 Gson();
}
@Provides
@Singleton
public CacheController provideCacheController() {
return new CacheController();
}
@Provides
@Singleton
public DBOpenHelper provideDBOpenHelper(Context context) {
@ -165,4 +149,10 @@ public class CommonsApplicationModule {
public LruCache<String, String> provideLruCache() {
return new LruCache<>(1024);
}
@Provides
@Singleton
public WikidataEditListener provideWikidataEditListener() {
return new WikidataEditListenerImpl();
}
}

View file

@ -4,6 +4,7 @@ import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.category.CategoryContentProvider;
import fr.free.nrw.commons.contributions.ContributionsContentProvider;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
import fr.free.nrw.commons.modifications.ModificationsContentProvider;
@Module
@ -19,4 +20,7 @@ public abstract class ContentProviderBuilderModule {
@ContributesAndroidInjector
abstract CategoryContentProvider bindCategoryContentProvider();
@ContributesAndroidInjector
abstract RecentSearchesContentProvider bindRecentSearchesContentProvider();
}

View file

@ -3,8 +3,12 @@ package fr.free.nrw.commons.di;
import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.category.SubCategoryListFragment;
import fr.free.nrw.commons.contributions.ContributionsListFragment;
import fr.free.nrw.commons.category.CategoryImagesListFragment;
import fr.free.nrw.commons.explore.categories.SearchCategoryFragment;
import fr.free.nrw.commons.explore.images.SearchImageFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.nearby.NearbyListFragment;
@ -51,4 +55,16 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract CategoryImagesListFragment bindFeaturedImagesListFragment();
@ContributesAndroidInjector
abstract SubCategoryListFragment bindSubCategoryListFragment();
@ContributesAndroidInjector
abstract SearchImageFragment bindBrowseImagesListFragment();
@ContributesAndroidInjector
abstract SearchCategoryFragment bindSearchCategoryListFragment();
@ContributesAndroidInjector
abstract RecentSearchesFragment bindRecentSearchesFragment();
}

View file

@ -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();
}
}

View file

@ -0,0 +1,243 @@
package fr.free.nrw.commons.explore;
import android.database.DataSetObserver;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.view.View;
import android.widget.FrameLayout;
import android.widget.SearchView;
import android.widget.Toast;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxSearchView;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.explore.categories.SearchCategoryFragment;
import fr.free.nrw.commons.explore.images.SearchImageFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
/**
* Represents search screen of this app
*/
public class SearchActivity extends NavigationBaseActivity implements MediaDetailPagerFragment.MediaDetailProvider{
@BindView(R.id.toolbar_search) Toolbar toolbar;
@BindView(R.id.searchHistoryContainer) FrameLayout searchHistoryContainer;
@BindView(R.id.mediaContainer) FrameLayout mediaContainer;
@BindView(R.id.searchBox) SearchView searchView;
@BindView(R.id.tabLayout) TabLayout tabLayout;
@BindView(R.id.viewPager) ViewPager viewPager;
private SearchImageFragment searchImageFragment;
private SearchCategoryFragment searchCategoryFragment;
private RecentSearchesFragment recentSearchesFragment;
private FragmentManager supportFragmentManager;
private MediaDetailPagerFragment mediaDetails;
ViewPagerAdapter viewPagerAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search);
ButterKnife.bind(this);
initDrawer();
setTitle(getString(R.string.title_activity_search));
toolbar.setNavigationOnClickListener(v->onBackPressed());
supportFragmentManager = getSupportFragmentManager();
setSearchHistoryFragment();
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(viewPagerAdapter);
tabLayout.setupWithViewPager(viewPager);
setTabs();
searchView.setQueryHint(getString(R.string.search_commons));
searchView.onActionViewExpanded();
searchView.clearFocus();
}
/**
* This method sets the search history fragment.
* Search history fragment is displayed when query is empty.
*/
private void setSearchHistoryFragment() {
recentSearchesFragment = new RecentSearchesFragment();
FragmentTransaction transaction = supportFragmentManager.beginTransaction();
transaction.add(R.id.searchHistoryContainer, recentSearchesFragment).commit();
}
/**
* Sets the titles in the tabLayout and fragments in the viewPager
*/
public void setTabs() {
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
searchImageFragment = new SearchImageFragment();
searchCategoryFragment= new SearchCategoryFragment();
fragmentList.add(searchImageFragment);
titleList.add("MEDIA");
fragmentList.add(searchCategoryFragment);
titleList.add("CATEGORIES");
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPagerAdapter.notifyDataSetChanged();
RxSearchView.queryTextChanges(searchView)
.takeUntil(RxView.detaches(searchView))
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe( query -> {
//update image list
if (!TextUtils.isEmpty(query)) {
viewPager.setVisibility(View.VISIBLE);
tabLayout.setVisibility(View.VISIBLE);
searchHistoryContainer.setVisibility(View.GONE);
searchImageFragment.updateImageList(query.toString());
searchCategoryFragment.updateCategoryList(query.toString());
}else {
viewPager.setVisibility(View.GONE);
tabLayout.setVisibility(View.GONE);
searchHistoryContainer.setVisibility(View.VISIBLE);
recentSearchesFragment.updateRecentSearches();
// open search history fragment
}
}
);
}
/**
* returns Media Object at position
* @param i position of Media in the imagesRecyclerView adapter.
*/
@Override
public Media getMediaAtPosition(int i) {
return searchImageFragment.getImageAtPosition(i);
}
/**
* returns total number of images present in the imagesRecyclerView adapter.
*/
@Override
public int getTotalMediaCount() {
return searchImageFragment.getTotalImagesCount();
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void notifyDatasetChanged() {
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void registerDataSetObserver(DataSetObserver observer) {
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
}
/**
* Open media detail pager fragment on click of image in search results
* @param index item index that should be opened
*/
public void onSearchImageClicked(int index) {
ViewUtil.hideKeyboard(this.findViewById(R.id.searchBox));
toolbar.setVisibility(View.GONE);
tabLayout.setVisibility(View.GONE);
viewPager.setVisibility(View.GONE);
mediaContainer.setVisibility(View.VISIBLE);
setNavigationBaseToolbarVisibility(true);
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()
.hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount()))
.add(R.id.mediaContainer, mediaDetails)
.addToBackStack(null)
.commit();
// Reason for using hide, add instead of replace is to maintain scroll position after
// coming back to the search activity. See https://github.com/commons-app/apps-android-commons/issues/1631
// https://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack/19022550#19022550
supportFragmentManager.executePendingTransactions();
}
mediaDetails.showImage(index);
forceInitBackButton();
}
/**
* This method is called on Screen Rotation
*/
@Override
protected void onResume() {
if (supportFragmentManager.getBackStackEntryCount()==1){
//FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time.
//FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894
// This is called on screen rotation when user is inside media details. Ideally it should show Media Details but since we are not saving the state now. We are throwing the user to search screen otherwise the app was crashing.
//
onBackPressed();
}
super.onResume();
}
/**
* This method is called on backPressed of anyFragment in the activity.
* If condition is called when mediaDetailFragment is opened.
*/
@Override
public void onBackPressed() {
if (getSupportFragmentManager().getBackStackEntryCount() == 1){
// back to search so show search toolbar and hide navigation toolbar
toolbar.setVisibility(View.VISIBLE);
tabLayout.setVisibility(View.VISIBLE);
viewPager.setVisibility(View.VISIBLE);
mediaContainer.setVisibility(View.GONE);
setNavigationBaseToolbarVisibility(false);
}else {
toolbar.setVisibility(View.GONE);
setNavigationBaseToolbarVisibility(true);
}
super.onBackPressed();
}
/**
* This method is called on click of a recent search to update query in SearchView.
* @param query Recent Search Query
*/
public void updateText(String query) {
searchView.setQuery(query, true);
// Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details.
// https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511
viewPager.requestFocus();
}
}

View file

@ -0,0 +1,57 @@
package fr.free.nrw.commons.explore;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import java.util.ArrayList;
import java.util.List;
/**
* This adapter will be used to display fragments in a ViewPager
*/
public class ViewPagerAdapter extends FragmentPagerAdapter {
private List<Fragment> fragmentList = new ArrayList<>();
private List<String> fragmentTitleList = new ArrayList<>();
public ViewPagerAdapter(FragmentManager manager) {
super(manager);
}
/**
* This method returns the fragment of the viewpager at a particular position
* @param position
*/
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
/**
* This method returns the total number of fragments in the viewpager.
* @return size
*/
@Override
public int getCount() {
return fragmentList.size();
}
/**
* This method sets the fragment and title list in the viewpager
* @param fragmentList List of all fragments to be displayed in the viewpager
* @param fragmentTitleList List of all titles of the fragments
*/
public void setTabData(List<Fragment> fragmentList, List<String> fragmentTitleList) {
this.fragmentList = fragmentList;
this.fragmentTitleList = fragmentTitleList;
}
/**
* This method returns the title of the page at a particular position
* @param position
*/
@Override
public CharSequence getPageTitle(int position) {
return fragmentTitleList.get(position);
}
}

View file

@ -0,0 +1,32 @@
package fr.free.nrw.commons.explore.categories;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder;
import java.util.Collections;
import java.util.List;
/**
* This class helps in creating adapter for categoriesRecyclerView in SearchCategoryFragment,
* implementing onClicks on categoriesRecyclerView Items
**/
public class SearchCategoriesAdapterFactory {
private final SearchCategoriesRenderer.CategoryClickedListener listener;
public SearchCategoriesAdapterFactory(SearchCategoriesRenderer.CategoryClickedListener listener) {
this.listener = listener;
}
/**
* This method creates a recyclerViewAdapter for Categories.
* @param searchImageItemList List of category name to be displayed
* @return categoriesAdapter
**/
public RVRendererAdapter<String> create(List<String> searchImageItemList) {
RendererBuilder<String> builder = new RendererBuilder<String>().bind(String.class, new SearchCategoriesRenderer(listener));
ListAdapteeCollection<String> collection = new ListAdapteeCollection<>(
searchImageItemList != null ? searchImageItemList : Collections.<String>emptyList());
return new RVRendererAdapter<>(builder, collection);
}
}

View file

@ -0,0 +1,56 @@
package fr.free.nrw.commons.explore.categories;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.pedrogomez.renderers.Renderer;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
/**
* presentation logic of individual category in search is handled here
**/
class SearchCategoriesRenderer extends Renderer<String> {
@BindView(R.id.textView1) TextView tvCategoryName;
private final CategoryClickedListener listener;
SearchCategoriesRenderer(CategoryClickedListener listener) {
this.listener = listener;
}
@Override
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
return layoutInflater.inflate(R.layout.item_recent_searches, viewGroup, false);
}
@Override
protected void setUpView(View view) {
ButterKnife.bind(this, view);
}
@Override
protected void hookListeners(View view) {
view.setOnClickListener(v -> {
String item = getContent();
if (listener != null) {
listener.categoryClicked(item);
}
});
}
@Override
public void render() {
String item = getContent();
tvCategoryName.setText(item.replaceFirst("^Category:", ""));
}
public interface CategoryClickedListener {
void categoryClicked(String item);
}
}

View file

@ -0,0 +1,222 @@
package fr.free.nrw.commons.explore.categories;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
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 the category search screen.
*/
public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
private static int TIMEOUT_SECONDS = 15;
@BindView(R.id.imagesListBox)
RecyclerView categoriesRecyclerView;
@BindView(R.id.imageSearchInProgress)
ProgressBar progressBar;
@BindView(R.id.imagesNotFound)
TextView categoriesNotFoundView;
String query;
@Inject RecentSearchesDao recentSearchesDao;
@Inject MediaWikiApi mwApi;
@Inject @Named("default_preferences") SharedPreferences prefs;
private RVRendererAdapter<String> categoriesAdapter;
private List<String> queryList = new ArrayList<>();
private final SearchCategoriesAdapterFactory adapterFactory = new SearchCategoriesAdapterFactory(item -> {
// Called on Click of a individual category Item
// Open Category Details activity
CategoryDetailsActivity.startYourself(getContext(), item);
saveQuery(query);
});
/**
* This method saves Search Query in the Recent Searches Database.
* @param query
*/
private void saveQuery(String query) {
RecentSearch recentSearch = recentSearchesDao.find(query);
// Newly searched query...
if (recentSearch == null) {
recentSearch = new RecentSearch(null, query, new Date());
}
else {
recentSearch.setLastSearched(new Date());
}
recentSearchesDao.save(recentSearch);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
ButterKnife.bind(this, rootView);
if(getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
}
else{
categoriesRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
}
ArrayList<String> items = new ArrayList<>();
categoriesAdapter = adapterFactory.create(items);
categoriesRecyclerView.setAdapter(categoriesAdapter);
categoriesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
// check if end of recycler view is reached, if yes then add more results to existing results
if (!recyclerView.canScrollVertically(1)) {
addCategoriesToList(query);
}
}
});
return rootView;
}
/**
* Checks for internet connection and then initializes the recycler view with 25 categories of the searched query
* Clearing categoryAdapter every time new keyword is searched so that user can see only new results
*/
public void updateCategoryList(String query) {
this.query = query;
categoriesNotFoundView.setVisibility(GONE);
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
progressBar.setVisibility(View.VISIBLE);
queryList.clear();
categoriesAdapter.clear();
Observable.fromCallable(() -> mwApi.searchCategory(query,queryList.size()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(this::handleSuccess, this::handleError);
}
/**
* Adds more results to existing search results
*/
public void addCategoriesToList(String query) {
this.query = query;
progressBar.setVisibility(View.VISIBLE);
Observable.fromCallable(() -> mwApi.searchCategory(query,queryList.size()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(this::handlePaginationSuccess, this::handleError);
}
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
* @param mediaList
*/
private void handlePaginationSuccess(List<String> mediaList) {
queryList.addAll(mediaList);
progressBar.setVisibility(View.GONE);
categoriesAdapter.addAll(mediaList);
categoriesAdapter.notifyDataSetChanged();
}
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
* @param mediaList
*/
private void handleSuccess(List<String> mediaList) {
queryList = mediaList;
if(mediaList == null || mediaList.isEmpty()) {
initErrorView();
}
else {
progressBar.setVisibility(View.GONE);
categoriesAdapter.addAll(mediaList);
categoriesAdapter.notifyDataSetChanged();
// check if user is waiting for 5 seconds if yes then save search query to history.
Handler handler = new Handler();
handler.postDelayed(() -> saveQuery(query), 5000);
}
}
/**
* Logs and handles API error scenario
* @param throwable
*/
private void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading queried categories");
try {
initErrorView();
ViewUtil.showSnackbar(categoriesRecyclerView, R.string.error_loading_categories);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* Handles the UI updates for a error scenario
*/
private void initErrorView() {
progressBar.setVisibility(GONE);
categoriesNotFoundView.setVisibility(VISIBLE);
categoriesNotFoundView.setText(getString(R.string.categories_not_found, query));
}
/**
* Handles the UI updates for no internet scenario
*/
private void handleNoInternet() {
progressBar.setVisibility(GONE);
ViewUtil.showSnackbar(categoriesRecyclerView, R.string.no_internet);
}
}

View file

@ -0,0 +1,245 @@
package fr.free.nrw.commons.explore.images;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.Handler;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
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 the image search screen.
*/
public class SearchImageFragment extends CommonsDaggerSupportFragment {
private static int TIMEOUT_SECONDS = 15;
@BindView(R.id.imagesListBox)
RecyclerView imagesRecyclerView;
@BindView(R.id.imageSearchInProgress)
ProgressBar progressBar;
@BindView(R.id.imagesNotFound)
TextView imagesNotFoundView;
String query;
@Inject RecentSearchesDao recentSearchesDao;
@Inject MediaWikiApi mwApi;
@Inject @Named("default_preferences") SharedPreferences prefs;
private RVRendererAdapter<Media> imagesAdapter;
private List<Media> queryList = new ArrayList<>();
private final SearchImagesAdapterFactory adapterFactory = new SearchImagesAdapterFactory(item -> {
// Called on Click of a individual media Item
int index = queryList.indexOf(item);
((SearchActivity)getContext()).onSearchImageClicked(index);
saveQuery(query);
});
/**
* This method saves Search Query in the Recent Searches Database.
* @param query
*/
private void saveQuery(String query) {
RecentSearch recentSearch = recentSearchesDao.find(query);
// Newly searched query...
if (recentSearch == null) {
recentSearch = new RecentSearch(null, query, new Date());
}
else {
recentSearch.setLastSearched(new Date());
}
recentSearchesDao.save(recentSearch);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
ButterKnife.bind(this, rootView);
if(getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
imagesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
}
else{
imagesRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
}
ArrayList<Media> items = new ArrayList<>();
imagesAdapter = adapterFactory.create(items);
imagesRecyclerView.setAdapter(imagesAdapter);
imagesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
// check if end of recycler view is reached, if yes then add more results to existing results
if (!recyclerView.canScrollVertically(1)) {
addImagesToList(query);
}
}
});
return rootView;
}
/**
* Checks for internet connection and then initializes the recycler view with 25 images of the searched query
* Clearing imageAdapter every time new keyword is searched so that user can see only new results
*/
public void updateImageList(String query) {
this.query = query;
imagesNotFoundView.setVisibility(GONE);
if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
progressBar.setVisibility(View.VISIBLE);
queryList.clear();
imagesAdapter.clear();
Observable.fromCallable(() -> mwApi.searchImages(query,queryList.size()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(this::handleSuccess, this::handleError);
}
/**
* Adds more results to existing search results
*/
public void addImagesToList(String query) {
this.query = query;
progressBar.setVisibility(View.VISIBLE);
Observable.fromCallable(() -> mwApi.searchImages(query,queryList.size()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
.subscribe(this::handlePaginationSuccess, this::handleError);
}
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
* @param mediaList List of media to be added
*/
private void handlePaginationSuccess(List<Media> mediaList) {
queryList.addAll(mediaList);
progressBar.setVisibility(View.GONE);
imagesAdapter.addAll(mediaList);
imagesAdapter.notifyDataSetChanged();
}
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
* @param mediaList List of media to be shown
*/
private void handleSuccess(List<Media> mediaList) {
queryList = mediaList;
if(mediaList == null || mediaList.isEmpty()) {
initErrorView();
}
else {
progressBar.setVisibility(View.GONE);
imagesAdapter.addAll(mediaList);
imagesAdapter.notifyDataSetChanged();
// check if user is waiting for 5 seconds if yes then save search query to history.
Handler handler = new Handler();
handler.postDelayed(() -> saveQuery(query), 5000);
}
}
/**
* Logs and handles API error scenario
* @param throwable
*/
private void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading queried images");
try {
initErrorView();
ViewUtil.showSnackbar(imagesRecyclerView, R.string.error_loading_images);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* Handles the UI updates for a error scenario
*/
private void initErrorView() {
progressBar.setVisibility(GONE);
imagesNotFoundView.setVisibility(VISIBLE);
imagesNotFoundView.setText(getString(R.string.images_not_found, query));
}
/**
* Handles the UI updates for no internet scenario
*/
private void handleNoInternet() {
progressBar.setVisibility(GONE);
ViewUtil.showSnackbar(imagesRecyclerView, R.string.no_internet);
}
/**
* returns total number of images present in the recyclerview adapter.
*/
public int getTotalImagesCount(){
if (imagesAdapter == null) {
return 0;
}
else {
return imagesAdapter.getItemCount();
}
}
/**
* returns Media Object at position
* @param i position of Media in the recyclerview adapter.
*/
public Media getImageAtPosition(int i) {
if (imagesAdapter.getItem(i).getFilename() == null) {
// not yet ready to return data
return null;
}
else {
return new Media(imagesAdapter.getItem(i).getFilename());
}
}
}

View file

@ -0,0 +1,35 @@
package fr.free.nrw.commons.explore.images;
import com.pedrogomez.renderers.ListAdapteeCollection;
import com.pedrogomez.renderers.RVRendererAdapter;
import com.pedrogomez.renderers.RendererBuilder;
import java.util.Collections;
import java.util.List;
import fr.free.nrw.commons.Media;
/**
* This class helps in creating adapter for imagesRecyclerView in SearchImagesFragment,
* implementing onClicks on imagesRecyclerView Items
**/
class SearchImagesAdapterFactory {
private final SearchImagesRenderer.ImageClickedListener listener;
SearchImagesAdapterFactory(SearchImagesRenderer.ImageClickedListener listener) {
this.listener = listener;
}
/**
* This method creates a recyclerViewAdapter for Media.
* @param searchImageItemList List of Media objects to be displayed
* @return imagesAdapter
**/
public RVRendererAdapter<Media> create(List<Media> searchImageItemList) {
RendererBuilder<Media> builder = new RendererBuilder<Media>()
.bind(Media.class, new SearchImagesRenderer(listener));
ListAdapteeCollection<Media> collection = new ListAdapteeCollection<>(
searchImageItemList != null ? searchImageItemList : Collections.<Media>emptyList());
return new RVRendererAdapter<>(builder, collection);
}
}

View file

@ -0,0 +1,75 @@
package fr.free.nrw.commons.explore.images;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.pedrogomez.renderers.Renderer;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.R;
/**
* presentation logic of individual image in search is handled here
**/
class SearchImagesRenderer extends Renderer<Media> {
@BindView(R.id.categoryImageTitle) TextView tvImageName;
@BindView(R.id.categoryImageAuthor) TextView categoryImageAuthor;
@BindView(R.id.categoryImageView)
MediaWikiImageView browseImage;
private final ImageClickedListener listener;
SearchImagesRenderer(ImageClickedListener listener) {
this.listener = listener;
}
@Override
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
return layoutInflater.inflate(R.layout.layout_category_images, viewGroup, false);
}
@Override
protected void setUpView(View view) {
ButterKnife.bind(this, view);
}
@Override
protected void hookListeners(View view) {
view.setOnClickListener(v -> {
Media item = getContent();
if (listener != null) {
listener.imageClicked(item);
}
});
}
@Override
public void render() {
Media item = getContent();
tvImageName.setText(item.getDisplayTitle());
browseImage.setMedia(item);
setAuthorView(item, categoryImageAuthor);
}
interface ImageClickedListener {
void imageClicked(Media item);
}
/**
* formats author name as "Uploaded by: authorName" and sets it in textview
*/
private void setAuthorView(Media item, TextView author) {
if (item.getCreator() != null && !item.getCreator().equals("")) {
author.setVisibility(View.GONE);
String uploadedByTemplate = getContext().getString(R.string.image_uploaded_by);
author.setText(String.format(uploadedByTemplate, item.getCreator()));
} else {
author.setVisibility(View.VISIBLE);
}
}
}

View file

@ -0,0 +1,70 @@
package fr.free.nrw.commons.explore.recentsearches;
import android.net.Uri;
import java.util.Date;
/**
* Represents a recently searched query
* Example - query = "butterfly"
*/
public class RecentSearch {
private Uri contentUri;
private String query;
private Date lastSearched;
/**
* Constructor
* @param contentUri the content URI for this query
* @param query query name
* @param lastSearched last searched date
*/
public RecentSearch(Uri contentUri, String query, Date lastSearched) {
this.contentUri = contentUri;
this.query = query;
this.lastSearched = lastSearched;
}
/**
* Gets query name
* @return query name
*/
public String getQuery() {
return query;
}
/**
* Gets last searched date
* @return Last searched date
*/
public Date getLastSearched() {
// warning: Date objects are mutable.
return (Date)lastSearched.clone();
}
/**
* Updates the last searched date
* @param lastSearched Last searched date
*/
public void setLastSearched(Date lastSearched) {
this.lastSearched = lastSearched;
}
/**
* Gets the content URI for this query
* @return content URI
*/
public Uri getContentUri() {
return contentUri;
}
/**
* Modifies the content URI - marking this query as already saved in the database
*
* @param contentUri the content URI
*/
public void setContentUri(Uri contentUri) {
this.contentUri = contentUri;
}
}

View file

@ -0,0 +1,202 @@
package fr.free.nrw.commons.explore.recentsearches;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import javax.inject.Inject;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
import timber.log.Timber;
import static android.content.UriMatcher.NO_MATCH;
import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS;
import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID;
import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.TABLE_NAME;
/**
* This class contains functions for executing queries for
* inserting, searching, deleting, editing recent searches in SqLite DB
**/
public class RecentSearchesContentProvider extends CommonsDaggerContentProvider {
public static final String RECENT_SEARCH_AUTHORITY = "fr.free.nrw.commons.explore.recentsearches.contentprovider";
// For URI matcher
private static final int RECENT_SEARCHES = 1;
private static final int RECENT_SEARCHES_ID = 2;
private static final String BASE_PATH = "recent_searches";
public static final Uri BASE_URI = Uri.parse("content://" + RECENT_SEARCH_AUTHORITY + "/" + BASE_PATH);
private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH);
static {
uriMatcher.addURI(RECENT_SEARCH_AUTHORITY, BASE_PATH, RECENT_SEARCHES);
uriMatcher.addURI(RECENT_SEARCH_AUTHORITY, BASE_PATH + "/#", RECENT_SEARCHES_ID);
}
public static Uri uriForId(int id) {
return Uri.parse(BASE_URI.toString() + "/" + id);
}
@Inject DBOpenHelper dbOpenHelper;
/**
* This functions executes query for searching recent searches in SqLite DB
**/
@SuppressWarnings("ConstantConditions")
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(TABLE_NAME);
int uriType = uriMatcher.match(uri);
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor;
switch (uriType) {
case RECENT_SEARCHES:
cursor = queryBuilder.query(db, projection, selection, selectionArgs,
null, null, sortOrder);
break;
case RECENT_SEARCHES_ID:
cursor = queryBuilder.query(db,
ALL_FIELDS,
"_id = ?",
new String[]{uri.getLastPathSegment()},
null,
null,
sortOrder
);
break;
default:
throw new IllegalArgumentException("Unknown URI" + uri);
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public String getType(@NonNull Uri uri) {
return null;
}
/**
* This functions executes query for inserting a recentSearch object in SqLite DB
**/
@SuppressWarnings("ConstantConditions")
@Override
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id;
switch (uriType) {
case RECENT_SEARCHES:
id = sqlDB.insert(TABLE_NAME, null, contentValues);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(BASE_URI + "/" + id);
}
/**
* This functions executes query for deleting a recentSearch object in SqLite DB
**/
@Override
public int delete(@NonNull Uri uri, String s, String[] strings) {
int rows;
int uriType = uriMatcher.match(uri);
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
switch (uriType) {
case RECENT_SEARCHES_ID:
Timber.d("Deleting recent searches id %s", uri.getLastPathSegment());
rows = db.delete(RecentSearchesDao.Table.TABLE_NAME,
"_id = ?",
new String[]{uri.getLastPathSegment()}
);
break;
default:
throw new IllegalArgumentException("Unknown URI" + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return rows;
}
/**
* This functions executes query for inserting multiple recentSearch objects in SqLite DB
**/
@SuppressWarnings("ConstantConditions")
@Override
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (RecentSearchesContentProvider)");
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
sqlDB.beginTransaction();
switch (uriType) {
case RECENT_SEARCHES:
for (ContentValues value : values) {
Timber.d("Inserting! %s", value);
sqlDB.insert(TABLE_NAME, null, value);
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
sqlDB.setTransactionSuccessful();
sqlDB.endTransaction();
getContext().getContentResolver().notifyChange(uri, null);
return values.length;
}
/**
* This functions executes query for updating a particular recentSearch object in SqLite DB
**/
@SuppressWarnings("ConstantConditions")
@Override
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.
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();
int rowsUpdated;
switch (uriType) {
case RECENT_SEARCHES_ID:
if (TextUtils.isEmpty(selection)) {
int id = Integer.valueOf(uri.getLastPathSegment());
rowsUpdated = sqlDB.update(TABLE_NAME,
contentValues,
COLUMN_ID + " = ?",
new String[]{String.valueOf(id)});
} else {
throw new IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID");
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType);
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsUpdated;
}
}

View file

@ -0,0 +1,235 @@
package fr.free.nrw.commons.explore.recentsearches;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.RemoteException;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
/**
* This class doesn't execute queries in database directly instead it contains the logic behind
* inserting, deleting, searching data from recent searches database.
**/
public class RecentSearchesDao {
private final Provider<ContentProviderClient> clientProvider;
@Inject
public RecentSearchesDao(@Named("recentsearch") Provider<ContentProviderClient> clientProvider) {
this.clientProvider = clientProvider;
}
/**
* This method is called on click of media/ categories for storing them in recent searches
* @param recentSearch a recent searches object that is to be added in SqLite DB
*/
public void save(RecentSearch recentSearch) {
ContentProviderClient db = clientProvider.get();
try {
if (recentSearch.getContentUri() == null) {
recentSearch.setContentUri(db.insert(RecentSearchesContentProvider.BASE_URI, toContentValues(recentSearch)));
} else {
db.update(recentSearch.getContentUri(), toContentValues(recentSearch), null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* This method is called on confirmation of delete recent searches.
* It deletes latest 10 recent searches from the database
* @param recentSearchesStringList list of recent searches to be deleted
*/
public void deleteAll(List<String> recentSearchesStringList) {
ContentProviderClient db = clientProvider.get();
for (String recentSearchName : recentSearchesStringList) {
try {
RecentSearch recentSearch = find(recentSearchName);
if (recentSearch.getContentUri() == null) {
throw new RuntimeException("tried to delete item with no content URI");
} else {
Log.d("QUERY_NAME",recentSearch.getContentUri()+"- delete tried");
db.delete(recentSearch.getContentUri(), null, null);
Log.d("QUERY_NAME",recentSearch.getQuery()+"- query deleted");
}
} catch (RemoteException e) {
Log.d("Exception",e+"- query deleted");
throw new RuntimeException(e);
} finally {
db.release();
}
}
}
/**
* Find persisted search query in database, based on its name.
* @param name Search query Ex- "butterfly"
* @return recently searched query from database, or null if not found
*/
@Nullable
public RecentSearch find(String name) {
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
RecentSearchesContentProvider.BASE_URI,
Table.ALL_FIELDS,
Table.COLUMN_NAME + "=?",
new String[]{name},
null);
if (cursor != null && cursor.moveToFirst()) {
return fromCursor(cursor);
}
} catch (RemoteException e) {
// This feels lazy, but to hell with checked exceptions. :)
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
db.release();
}
return null;
}
/**
* Retrieve recently-searched queries, ordered by descending date.
* @return a list containing recent searches
*/
@NonNull
public List<String> recentSearches(int limit) {
List<String> items = new ArrayList<>();
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query( RecentSearchesContentProvider.BASE_URI, Table.ALL_FIELDS,
null, new String[]{}, Table.COLUMN_LAST_USED + " DESC");
// fixme add a limit on the original query instead of falling out of the loop?
while (cursor != null && cursor.moveToNext() && cursor.getPosition() < limit) {
items.add(fromCursor(cursor).getQuery());
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
db.release();
}
return items;
}
/**
* It creates an Recent Searches object from data stored in the SQLite DB by using cursor
* @param cursor
* @return RecentSearch object
*/
@NonNull
RecentSearch fromCursor(Cursor cursor) {
// Hardcoding column positions!
return new RecentSearch(
RecentSearchesContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))),
cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)),
new Date(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LAST_USED)))
);
}
/**
* This class contains the database table architechture for recent searches,
* It also contains queries and logic necessary to the create, update, delete this table.
*/
private ContentValues toContentValues(RecentSearch recentSearch) {
ContentValues cv = new ContentValues();
cv.put(RecentSearchesDao.Table.COLUMN_NAME, recentSearch.getQuery());
cv.put(RecentSearchesDao.Table.COLUMN_LAST_USED, recentSearch.getLastSearched().getTime());
return cv;
}
/**
* This class contains the database table architechture for recent searches,
* It also contains queries and logic necessary to the create, update, delete this table.
*/
public static class Table {
public static final String TABLE_NAME = "recent_searches";
public static final String COLUMN_ID = "_id";
static final String COLUMN_NAME = "name";
static final String COLUMN_LAST_USED = "last_used";
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = {
COLUMN_ID,
COLUMN_NAME,
COLUMN_LAST_USED,
};
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
+ COLUMN_NAME + " STRING,"
+ COLUMN_LAST_USED + " INTEGER"
+ ");";
/**
* This method creates a RecentSearchesTable in SQLiteDatabase
* @param db SQLiteDatabase
*/
public static void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
}
/**
* This method deletes RecentSearchesTable from SQLiteDatabase
* @param db SQLiteDatabase
*/
public static void onDelete(SQLiteDatabase db) {
db.execSQL(DROP_TABLE_STATEMENT);
onCreate(db);
}
/**
* This method is called on migrating from a older version to a newer version
* @param db SQLiteDatabase
* @param from Version from which we are migrating
* @param to Version to which we are migrating
*/
public static void onUpdate(SQLiteDatabase db, int from, int to) {
if (from == to) {
return;
}
if (from < 6) {
// doesn't exist yet
from++;
onUpdate(db, from, to);
return;
}
if (from == 6) {
// table added in version 7
onCreate(db);
from++;
onUpdate(db, from, to);
return;
}
if (from == 7) {
from++;
onUpdate(db, from, to);
return;
}
}
}
}

View file

@ -0,0 +1,85 @@
package fr.free.nrw.commons.explore.recentsearches;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.Toast;
import java.util.List;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.SearchActivity;
/**
* Displays the recent searches screen.
*/
public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
@Inject RecentSearchesDao recentSearchesDao;
@BindView(R.id.recent_searches_list) ListView recentSearchesList;
List<String> recentSearches;
ArrayAdapter adapter;
@BindView(R.id.recent_searches_delete_button)
ImageView recent_searches_delete_button;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_search_history, container, false);
ButterKnife.bind(this, rootView);
recentSearches = recentSearchesDao.recentSearches(10);
recent_searches_delete_button.setOnClickListener(v -> {
new AlertDialog.Builder(getContext())
.setMessage(getString(R.string.delete_recent_searches_dialog))
.setPositiveButton("YES", (dialog, which) -> {
recentSearchesDao.deleteAll(recentSearches);
Toast.makeText(getContext(),getString(R.string.search_history_deleted),Toast.LENGTH_SHORT).show();
recentSearches = recentSearchesDao.recentSearches(10);
adapter = new ArrayAdapter<String>(getContext(),R.layout.item_recent_searches, recentSearches);
recentSearchesList.setAdapter(adapter);
adapter.notifyDataSetChanged();
dialog.dismiss();
})
.setNegativeButton("NO", null)
.create()
.show();
});
adapter = new ArrayAdapter<String>(getContext(),R.layout.item_recent_searches, recentSearches);
recentSearchesList.setAdapter(adapter);
recentSearchesList.setOnItemClickListener((parent, view, position, id) -> (
(SearchActivity)getContext()).updateText(recentSearches.get(position)));
adapter.notifyDataSetChanged();
return rootView;
}
/**
* This method is called on back press of activity
* so we are updating the list from database to refresh the recent searches list.
*/
@Override
public void onResume() {
recentSearches = recentSearchesDao.recentSearches(10);
adapter.notifyDataSetChanged();
super.onResume();
}
/**
* This method is called when search query is null to update Recent Searches
*/
public void updateRecentSearches() {
recentSearches = recentSearchesDao.recentSearches(10);
adapter = new ArrayAdapter<String>(getContext(),R.layout.item_recent_searches, recentSearches);
recentSearchesList.setAdapter(adapter);
adapter.notifyDataSetChanged();
}
}

View file

@ -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<InputStream, SVG> {
@Override
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
// TODO: Can we tell?
return true;
}
public Resource<SVG> decode(@NonNull InputStream source, int width, int height,
@NonNull Options options)
throws IOException {
try {
SVG svg = SVG.getFromInputStream(source);
return new SimpleResource<>(svg);
} catch (SVGParseException ex) {
throw new IOException("Cannot load SVG from stream", ex);
}
}
}

View file

@ -0,0 +1,28 @@
package fr.free.nrw.commons.glide;
import android.graphics.Picture;
import android.graphics.drawable.PictureDrawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.engine.Resource;
import com.bumptech.glide.load.resource.SimpleResource;
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
import com.caverock.androidsvg.SVG;
/**
* Convert the {@link SVG}'s internal representation to an Android-compatible one
* ({@link Picture}).
*/
public class SvgDrawableTranscoder implements ResourceTranscoder<SVG, PictureDrawable> {
@Nullable
@Override
public Resource<PictureDrawable> transcode(@NonNull Resource<SVG> toTranscode,
@NonNull Options options) {
SVG svg = toTranscode.get();
Picture picture = svg.renderToPicture();
PictureDrawable drawable = new PictureDrawable(picture);
return new SimpleResource<>(drawable);
}
}

View file

@ -0,0 +1,51 @@
package fr.free.nrw.commons.glide;
import android.graphics.drawable.PictureDrawable;
import android.widget.ImageView;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.ImageViewTarget;
import com.bumptech.glide.request.target.Target;
/**
* Listener which updates the {@link ImageView} to be software rendered, because
* {@link com.caverock.androidsvg.SVG SVG}/{@link android.graphics.Picture Picture} can't render on
* a hardware backed {@link android.graphics.Canvas Canvas}.
*/
public class SvgSoftwareLayerSetter implements RequestListener<PictureDrawable> {
/**
* Sets the layer type to none if the load fails
* @param e
* @param model
* @param target
* @param isFirstResource
* @return
*/
@Override
public boolean onLoadFailed(GlideException e, Object model, Target<PictureDrawable> target,
boolean isFirstResource) {
ImageView view = ((ImageViewTarget<?>) target).getView();
view.setLayerType(ImageView.LAYER_TYPE_NONE, null);
return false;
}
/**
* Sets the layer type to software when the resource is ready
* @param resource
* @param model
* @param target
* @param dataSource
* @param isFirstResource
* @return
*/
@Override
public boolean onResourceReady(PictureDrawable resource, Object model,
Target<PictureDrawable> target, DataSource dataSource, boolean isFirstResource) {
ImageView view = ((ImageViewTarget<?>) target).getView();
view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null);
return false;
}
}

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.location;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
@ -10,9 +11,10 @@ import android.location.LocationManager;
import android.os.Bundle;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import timber.log.Timber;
@ -29,6 +31,7 @@ public class LocationServiceManager implements LocationListener {
private Location lastLocation;
private final List<LocationUpdateListener> locationListeners = new CopyOnWriteArrayList<>();
private boolean isLocationManagerRegistered = false;
private Set<Activity> locationExplanationDisplayed = new HashSet<>();
/**
* Constructs a new instance of LocationServiceManager.
@ -51,7 +54,6 @@ public class LocationServiceManager implements LocationListener {
/**
* Returns whether the location permission is granted.
*
* @return true if the location permission is granted
*/
public boolean isLocationPermissionGranted() {
@ -73,10 +75,23 @@ public class LocationServiceManager implements LocationListener {
LOCATION_REQUEST);
}
/**
* The permission explanation dialog box is now displayed just once for a particular activity. We are subscribing
* to updates from multiple providers so its important to show the dialog just once. Otherwise it will be displayed
* once for every provider, which in our case currently is 2.
* @param activity
* @return
*/
public boolean isPermissionExplanationRequired(Activity activity) {
return !activity.isFinishing() &&
ActivityCompat.shouldShowRequestPermissionRationale(activity,
Manifest.permission.ACCESS_FINE_LOCATION);
if (activity.isFinishing()) {
return false;
}
boolean showRequestPermissionRationale = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_FINE_LOCATION);
if (showRequestPermissionRationale && !locationExplanationDisplayed.contains(activity)) {
locationExplanationDisplayed.add(activity);
return true;
}
return false;
}
/**
@ -84,8 +99,9 @@ public class LocationServiceManager implements LocationListener {
* (e.g. when Location permission just granted)
* @return last known LatLng
*/
@SuppressLint("MissingPermission")
public LatLng getLKL() {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
if (isLocationPermissionGranted()) {
Location lastKL = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
if (lastKL == null) {
lastKL = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
@ -107,9 +123,10 @@ public class LocationServiceManager implements LocationListener {
* Registers a LocationManager to listen for current location.
*/
public void registerLocationManager() {
if (!isLocationManagerRegistered)
if (!isLocationManagerRegistered) {
isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
&& requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER);
}
}
/**
@ -142,7 +159,7 @@ public class LocationServiceManager implements LocationListener {
* @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly
* LOCATION_SLIGHTLY_CHANGED if location changed slightly
*/
protected LocationChangeType isBetterLocation(Location location, Location currentBestLocation) {
private LocationChangeType isBetterLocation(Location location, Location currentBestLocation) {
if (currentBestLocation == null) {
// A new location is always better than no location
@ -267,6 +284,7 @@ public class LocationServiceManager implements LocationListener {
LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers
LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving
LOCATION_NOT_CHANGED,
PERMISSION_JUST_GRANTED
PERMISSION_JUST_GRANTED,
MAP_UPDATED
}
}

View file

@ -42,6 +42,7 @@ import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.MediaWikiImageView;
import fr.free.nrw.commons.PageTitle;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.delete.DeleteTask;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.location.LatLng;
@ -56,16 +57,16 @@ import static android.widget.Toast.LENGTH_SHORT;
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private boolean editable;
private boolean isFeaturedMedia;
private boolean isCategoryImage;
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
private int index;
public static MediaDetailFragment forMedia(int index, boolean editable, boolean isFeaturedMedia) {
public static MediaDetailFragment forMedia(int index, boolean editable, boolean isCategoryImage) {
MediaDetailFragment mf = new MediaDetailFragment();
Bundle state = new Bundle();
state.putBoolean("editable", editable);
state.putBoolean("isFeaturedMedia", isFeaturedMedia);
state.putBoolean("isCategoryImage", isCategoryImage);
state.putInt("index", index);
state.putInt("listIndex", 0);
state.putInt("listTop", 0);
@ -128,7 +129,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
super.onSaveInstanceState(outState);
outState.putInt("index", index);
outState.putBoolean("editable", editable);
outState.putBoolean("isFeaturedMedia", isFeaturedMedia);
outState.putBoolean("isCategoryImage", isCategoryImage);
getScrollPosition();
outState.putInt("listTop", initialListTop);
@ -144,12 +145,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if (savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable");
isFeaturedMedia = savedInstanceState.getBoolean("isFeaturedMedia");
isCategoryImage = savedInstanceState.getBoolean("isCategoryImage");
index = savedInstanceState.getInt("index");
initialListTop = savedInstanceState.getInt("listTop");
} else {
editable = getArguments().getBoolean("editable");
isFeaturedMedia = getArguments().getBoolean("isFeaturedMedia");
isCategoryImage = getArguments().getBoolean("isCategoryImage");
index = getArguments().getInt("index");
initialListTop = 0;
}
@ -161,7 +162,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
ButterKnife.bind(this,view);
if (isFeaturedMedia){
if (isCategoryImage){
authorLayout.setVisibility(VISIBLE);
} else {
authorLayout.setVisibility(GONE);
@ -245,6 +246,10 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@Override
protected Boolean doInBackground(Void... voids) {
// Local files have no filename yet
if(media.getFilename() == null) {
return Boolean.FALSE;
}
try {
extractor.fetch(media.getFilename(), licenseList);
return Boolean.TRUE;
@ -328,7 +333,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if (!TextUtils.isEmpty(licenseLink(media))) {
openWebBrowser(licenseLink(media));
} else {
if(isFeaturedMedia) {
if(isCategoryImage) {
Timber.d("Unable to fetch license URL for %s", media.getLicense());
} else {
Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT);
@ -426,17 +431,11 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
textView.setText(catName);
if (categoriesLoaded && categoriesPresent) {
textView.setOnClickListener(view -> {
// Open Category Details page
String selectedCategoryTitle = "Category:" + catName;
Intent viewIntent = new Intent();
viewIntent.setAction(Intent.ACTION_VIEW);
viewIntent.setData(new PageTitle(selectedCategoryTitle).getCanonicalUri());
//check if web browser available
if (viewIntent.resolveActivity(getActivity().getPackageManager()) != null) {
startActivity(viewIntent);
} else {
Toast toast = Toast.makeText(getContext(), getString(R.string.no_web_browser), LENGTH_SHORT);
toast.show();
}
Intent intent = new Intent(getContext(), CategoryDetailsActivity.class);
intent.putExtra("categoryName", selectedCategoryTitle);
getContext().startActivity(intent);
});
}
return item;
@ -503,8 +502,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if (media.getRequestedDeletion()){
delete.setVisibility(GONE);
nominatedForDeletion.setVisibility(VISIBLE);
}
else{
} else if (!isCategoryImage) {
delete.setVisibility(VISIBLE);
nominatedForDeletion.setVisibility(GONE);
}

View file

@ -9,6 +9,7 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.Fragment;
@ -38,6 +39,8 @@ import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.contributions.ContributionsActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.utils.ImageUtils;
import timber.log.Timber;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.content.Context.DOWNLOAD_SERVICE;
@ -120,7 +123,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
Media m = provider.getMediaAtPosition(pager.getCurrentItem());
switch (item.getItemId()) {
case R.id.menu_share_current_image:
// Share - intent set in onCreateOptionsMenu, around line 252
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_TEXT, m.getDisplayTitle() + " \n" + m.getFilePageTitle().getCanonicalUri());
startActivity(Intent.createChooser(shareIntent, "Share image via..."));
return true;
case R.id.menu_browser_current_image:
// View in browser
@ -140,6 +146,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
// Download
downloadMedia(m);
return true;
case R.id.menu_set_as_wallpaper:
// Set wallpaper
setWallpaper(m);
return true;
case R.id.menu_retry_current_image:
// Retry
((ContributionsActivity) getActivity()).retryUpload(pager.getCurrentItem());
@ -155,6 +165,19 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
}
}
/**
* Set the media as the device's wallpaper if the imageUrl is not null
* Fails silently if setting the wallpaper fails
* @param media
*/
private void setWallpaper(Media media) {
if(media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
Timber.d("Media URL not present");
return;
}
ImageUtils.setWallpaperFromImageUrl(getActivity(), Uri.parse(media.getImageUrl()));
}
/**
* Start the media file downloading to the local SD card/storage.
* The file can then be opened in Gallery or other apps.
@ -216,19 +239,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_download_current_image).setEnabled(true).setVisible(true);
// Set ShareActionProvider Intent
ShareActionProvider mShareActionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider(menu.findItem(R.id.menu_share_current_image));
// On some phones null is returned for some reason:
// https://github.com/commons-app/apps-android-commons/issues/413
if (mShareActionProvider != null) {
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_TEXT,
m.getDisplayTitle() + " \n" + m.getFilePageTitle().getCanonicalUri());
mShareActionProvider.setShareIntent(shareIntent);
}
if (m instanceof Contribution) {
if (m instanceof Contribution ) {
Contribution c = (Contribution) m;
switch (c.getState()) {
case Contribution.STATE_FAILED:
@ -257,7 +268,8 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
}
public void showImage(int i) {
pager.setCurrentItem(i);
Handler handler = new Handler();
handler.postDelayed(() -> pager.setCurrentItem(i), 10);
}
@Override

View file

@ -23,8 +23,11 @@ import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.util.EntityUtils;
import org.json.JSONObject;
import org.mediawiki.api.ApiResult;
import org.mediawiki.api.MWApi;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.io.IOException;
@ -33,10 +36,12 @@ import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.Callable;
import fr.free.nrw.commons.BuildConfig;
@ -49,6 +54,10 @@ import fr.free.nrw.commons.notification.NotificationUtils;
import in.yuvi.http.fluent.Http;
import io.reactivex.Observable;
import io.reactivex.Single;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import timber.log.Timber;
import static fr.free.nrw.commons.utils.ContinueUtils.getQueryContinue;
@ -62,6 +71,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private static final String THUMB_SIZE = "640";
private AbstractHttpClient httpClient;
private MWApi api;
private MWApi wikidataApi;
private Context context;
private SharedPreferences defaultPreferences;
private SharedPreferences categoryPreferences;
@ -69,6 +79,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
public ApacheHttpClientMediaWikiApi(Context context,
String apiURL,
String wikidatApiURL,
SharedPreferences defaultPreferences,
SharedPreferences categoryPreferences,
Gson gson) {
@ -82,6 +93,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent());
httpClient = new DefaultHttpClient(cm, params);
api = new MWApi(apiURL, httpClient);
wikidataApi = new MWApi(wikidatApiURL, httpClient);
this.defaultPreferences = defaultPreferences;
this.categoryPreferences = categoryPreferences;
this.gson = gson;
@ -206,6 +218,15 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return api.getEditToken();
}
@Override
public String getCentralAuthToken() throws IOException {
String centralAuthToken = api.action("centralauthtoken")
.get()
.getString("/api/centralauthtoken/@centralauthtoken");
Timber.d("MediaWiki Central auth token is %s", centralAuthToken);
return centralAuthToken;
}
@Override
public boolean fileExistsWithName(String fileName) throws IOException {
return api.action("query")
@ -351,6 +372,98 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
}).flatMapObservable(Observable::fromIterable);
}
/**
* Get the edit token for making wiki data edits
* https://www.mediawiki.org/wiki/API:Tokens
* @return
* @throws IOException
*/
private String getWikidataEditToken() throws IOException {
return wikidataApi.getEditToken();
}
@Override
public String getWikidataCsrfToken() throws IOException {
String wikidataCsrfToken = wikidataApi.action("query")
.param("action", "query")
.param("centralauthtoken", getCentralAuthToken())
.param("meta", "tokens")
.post()
.getString("/api/query/tokens/@csrftoken");
Timber.d("Wikidata csrf token is %s", wikidataCsrfToken);
return wikidataCsrfToken;
}
/**
* Creates a new claim using the wikidata API
* https://www.mediawiki.org/wiki/Wikibase/API
* @param entityId the wikidata entity to be edited
* @param property the property to be edited, for eg P18 for images
* @param snaktype the type of value stored for that property
* @param value the actual value to be stored for the property, for eg filename in case of P18
* @return returns revisionId if the claim is successfully created else returns null
* @throws IOException
*/
@Nullable
@Override
public String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException {
Timber.d("Filename is %s", value);
ApiResult result = wikidataApi.action("wbcreateclaim")
.param("entity", entityId)
.param("centralauthtoken", getCentralAuthToken())
.param("token", getWikidataCsrfToken())
.param("snaktype", snaktype)
.param("property", property)
.param("value", value)
.post();
if (result == null || result.getNode("api") == null) {
return null;
}
Node node = result.getNode("api").getDocument();
Element element = (Element) node;
if (element != null && element.getAttribute("success").equals("1")) {
return result.getString("api/pageinfo/@lastrevid");
} else {
Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info"));
}
return null;
}
/**
* Adds the wikimedia-commons-app tag to the edits made on wikidata
* @param revisionId
* @return
* @throws IOException
*/
@Nullable
@Override
public boolean addWikidataEditTag(String revisionId) throws IOException {
ApiResult result = wikidataApi.action("tag")
.param("revid", revisionId)
.param("centralauthtoken", getCentralAuthToken())
.param("token", getWikidataCsrfToken())
.param("add", "wikimedia-commons-app")
.param("reason", "Add tag for edits made using Android Commons app")
.post();
if (result == null || result.getNode("api") == null) {
return false;
}
Node node = result.getNode("api").getDocument();
Element element = (Element) node;
if (element != null && element.getAttribute("status").equals("success")) {
return true;
} else {
Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info"));
}
return false;
}
@Override
@NonNull
public Observable<String> searchTitles(String title, int searchCatsLimit) {
@ -444,8 +557,8 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
.param("notprop", "list")
.param("format", "xml")
.param("meta", "notifications")
// .param("meta", "notifications")
.param("notformat", "model")
.param("notwikis", "wikidatawiki|commonswiki|enwiki")
.get()
.getNode("/api/query/notifications/list");
} catch (IOException e) {
@ -464,32 +577,26 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
}
/**
* The method takes categoryName as input and returns a List of Media objects
* It uses the generator query API to get the images in a category, 10 at a time.
* The method takes categoryName as input and returns a List of Subcategories
* It uses the generator query API to get the subcategories in a category, 500 at a time.
* Uses the query continue values for fetching paginated responses
* @param categoryName Category name as defined on commons
* @return
*/
@Override
@NonNull
public List<Media> getCategoryImages(String categoryName) {
public List<String> getSubCategoryList(String categoryName) {
ApiResult apiResult = null;
try {
MWApi.RequestBuilder requestBuilder = api.action("query")
.param("generator", "categorymembers")
.param("format", "xml")
.param("gcmtype", "file")
.param("gcmtype","subcat")
.param("gcmtitle", categoryName)
.param("prop", "imageinfo")
.param("gcmlimit", "10")
.param("prop", "info")
.param("gcmlimit", "500")
.param("iiprop", "url|extmetadata");
QueryContinue queryContinueValues = getQueryContinueValues(categoryName);
if (queryContinueValues != null) {
requestBuilder.param("continue", queryContinueValues.getContinueParam());
requestBuilder.param("gcmcontinue", queryContinueValues.getGcmContinueParam());
}
apiResult = requestBuilder.get();
} catch (IOException e) {
Timber.e("Failed to obtain searchCategories", e);
@ -507,13 +614,183 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return new ArrayList<>();
}
QueryContinue queryContinue = getQueryContinue(apiResult.getNode("/api/continue").getDocument());
setQueryContinueValues(categoryName, queryContinue);
NodeList childNodes = categoryImagesNode.getDocument().getChildNodes();
return CategoryImageUtils.getSubCategoryList(childNodes);
}
/**
* The method takes categoryName as input and returns a List of parent categories
* It uses the generator query API to get the parent categories of a category, 500 at a time.
* @param categoryName Category name as defined on commons
* @return
*/
@Override
@NonNull
public List<String> getParentCategoryList(String categoryName) {
ApiResult apiResult = null;
try {
MWApi.RequestBuilder requestBuilder = api.action("query")
.param("generator", "categories")
.param("format", "xml")
.param("titles", categoryName)
.param("prop", "info")
.param("cllimit", "500")
.param("iiprop", "url|extmetadata");
apiResult = requestBuilder.get();
} catch (IOException e) {
Timber.e("Failed to obtain parent Categories", e);
}
if (apiResult == null) {
return new ArrayList<>();
}
ApiResult categoryImagesNode = apiResult.getNode("/api/query/pages");
if (categoryImagesNode == null
|| categoryImagesNode.getDocument() == null
|| categoryImagesNode.getDocument().getChildNodes() == null
|| categoryImagesNode.getDocument().getChildNodes().getLength() == 0) {
return new ArrayList<>();
}
NodeList childNodes = categoryImagesNode.getDocument().getChildNodes();
return CategoryImageUtils.getSubCategoryList(childNodes);
}
/**
* The method takes categoryName as input and returns a List of Media objects
* It uses the generator query API to get the images in a category, 10 at a time.
* Uses the query continue values for fetching paginated responses
* @param categoryName Category name as defined on commons
* @return
*/
@Override
@NonNull
public List<Media> getCategoryImages(String categoryName) {
ApiResult apiResult = null;
try {
MWApi.RequestBuilder requestBuilder = api.action("query")
.param("generator", "categorymembers")
.param("format", "xml")
.param("gcmtype", "file")
.param("gcmtitle", categoryName)
.param("gcmsort", "timestamp")//property to sort by;timestamp
.param("gcmdir", "desc")//in which direction to sort;descending
.param("prop", "imageinfo")
.param("gcmlimit", "10")
.param("iiprop", "url|extmetadata");
QueryContinue queryContinueValues = getQueryContinueValues(categoryName);
if (queryContinueValues != null) {
requestBuilder.param("continue", queryContinueValues.getContinueParam());
requestBuilder.param("gcmcontinue", queryContinueValues.getGcmContinueParam());
}
apiResult = requestBuilder.get();
} catch (IOException e) {
Timber.e("Failed to obtain searchCategories", e);
}
if (apiResult == null) {
return new ArrayList<>();
}
ApiResult categoryImagesNode = apiResult.getNode("/api/query/pages");
if (categoryImagesNode == null
|| categoryImagesNode.getDocument() == null
|| categoryImagesNode.getDocument().getChildNodes() == null
|| categoryImagesNode.getDocument().getChildNodes().getLength() == 0) {
return new ArrayList<>();
}
if (apiResult.getNode("/api/continue").getDocument()==null){
setQueryContinueValues(categoryName, null);
}else {
QueryContinue queryContinue = getQueryContinue(apiResult.getNode("/api/continue").getDocument());
setQueryContinueValues(categoryName, queryContinue);
}
NodeList childNodes = categoryImagesNode.getDocument().getChildNodes();
return CategoryImageUtils.getMediaList(childNodes);
}
/**
* This method takes search keyword as input and returns a list of Media objects filtered using search query
* It uses the generator query API to get the images searched using a query, 25 at a time.
* @param query keyword to search images on commons
* @return
*/
@Override
@NonNull
public List<Media> searchImages(String query, int offset) {
List<ApiResult> imageNodes = null;
try {
imageNodes = api.action("query")
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "6")
.param("srlimit", "25")
.param("sroffset",offset)
.param("srsearch", query)
.get()
.getNodes("/api/query/search/p/@title");
} catch (IOException e) {
Timber.e("Failed to obtain searchImages", e);
}
if (imageNodes == null) {
return new ArrayList<Media>();
}
List<Media> images = new ArrayList<>();
for (ApiResult imageNode : imageNodes) {
String imgName = imageNode.getDocument().getTextContent();
images.add(new Media(imgName));
}
return images;
}
/**
* This method takes search keyword as input and returns a list of categories objects filtered using search query
* It uses the generator query API to get the categories searched using a query, 25 at a time.
* @param query keyword to search categories on commons
* @return
*/
@Override
@NonNull
public List<String> searchCategory(String query, int offset) {
List<ApiResult> categoryNodes = null;
try {
categoryNodes = api.action("query")
.param("format", "xml")
.param("list", "search")
.param("srwhat", "text")
.param("srnamespace", "14")
.param("srlimit", "25")
.param("sroffset",offset)
.param("srsearch", query)
.get()
.getNodes("/api/query/search/p/@title");
} catch (IOException e) {
Timber.e("Failed to obtain searchCategories", e);
}
if (categoryNodes == null) {
return new ArrayList<String>();
}
List<String> categories = new ArrayList<>();
for (ApiResult categoryNode : categoryNodes) {
String catName = categoryNode.getDocument().getTextContent();
categories.add(catName);
}
return categories;
}
/**
* 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
@ -586,6 +863,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
String resultStatus = result.getString("/api/upload/@result");
if (!resultStatus.equals("Success")) {
String errorCode = result.getString("/api/error/@code");
Timber.e(errorCode);
return new UploadResult(resultStatus, errorCode);
} else {
Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp"));
@ -600,7 +878,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
@NonNull
public Single<Integer> getUploadCount(String userName) {
final String uploadCountUrlTemplate =
wikiMediaToolforgeUrl + "urbanecmbot/uploadsbyuser/uploadsbyuser.py";
wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/uploadsbyuser.py";
return Single.fromCallable(() -> {
String url = String.format(
@ -615,12 +893,108 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
});
}
/**
* Checks to see if a user is currently blocked from Commons
* @return whether or not the user is blocked from Commons
*/
@Override
public boolean isUserBlockedFromCommons() {
boolean userBlocked = false;
try {
ApiResult result = api.action("query")
.param("action", "query")
.param("format", "xml")
.param("meta", "userinfo")
.param("uiprop", "blockinfo")
.get();
if (result != null) {
String blockEnd = result.getString("/api/query/userinfo/@blockexpiry");
if (blockEnd.equals("infinite")) {
userBlocked = true;
} else if (!blockEnd.isEmpty()) {
Date endDate = parseMWDate(blockEnd);
Date current = new Date();
userBlocked = endDate.after(current);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return userBlocked;
}
/**
* This takes userName as input, which is then used to fetch the feedback/achievements
* statistics using OkHttp and JavaRx. This function return JSONObject
* @param userName
* @return
*/
@NonNull
@Override
public Single<JSONObject> getAchievements(String userName) {
final String fetchAchievementUrlTemplate =
wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/feedback.py";
return Single.fromCallable(() -> {
String url = String.format(
Locale.ENGLISH,
fetchAchievementUrlTemplate,
new PageTitle(userName).getText());
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
Log.i("url", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
OkHttpClient client = new OkHttpClient();
Response response = client.newCall(request).execute();
String jsonData = response.body().string();
JSONObject jsonObject = new JSONObject(jsonData);
return jsonObject;
});
}
/**
* This takes userName as input, which is then used to fetch the no of images deleted
* using OkHttp and JavaRx. This function return JSONObject
* @param userName
* @return
*/
@NonNull
@Override
public Single<JSONObject> getRevertCount(String userName){
final String fetchRevertCountUrlTemplate =
wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/feedback.py";
return Single.fromCallable(() -> {
String url = String.format(
Locale.ENGLISH,
fetchRevertCountUrlTemplate,
new PageTitle(userName).getText());
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
urlBuilder.addQueryParameter("fetch","deletedUploads");
Log.i("url", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
OkHttpClient client = new OkHttpClient();
Response response = client.newCall(request).execute();
String jsonData = response.body().string();
JSONObject jsonRevertObject = new JSONObject(jsonData);
return jsonRevertObject;
});
}
private Date parseMWDate(String mwDate) {
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC
isoFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
try {
return isoFormat.parse(mwDate);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,101 @@
package fr.free.nrw.commons.mwapi;
import com.google.gson.Gson;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.mwapi.model.ApiResponse;
import fr.free.nrw.commons.mwapi.model.Page;
import fr.free.nrw.commons.mwapi.model.PageCategory;
import io.reactivex.Single;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import timber.log.Timber;
/**
* Uses the OkHttp library to implement calls to the Commons MediaWiki API to match GPS coordinates
* with nearby Commons categories. Parses the results using GSON to obtain a list of relevant
* categories. Note: that caller is responsible for executing the request() method on a background
* thread.
*/
public class CategoryApi {
private final OkHttpClient okHttpClient;
private final HttpUrl mwUrl;
private final Gson gson;
@Inject
public CategoryApi(OkHttpClient okHttpClient, Gson gson,
@Named("commons_mediawiki_url") HttpUrl mwUrl) {
this.okHttpClient = okHttpClient;
this.mwUrl = mwUrl;
this.gson = gson;
}
public Single<List<String>> request(String coords) {
return Single.fromCallable(() -> {
HttpUrl apiUrl = buildUrl(coords);
Timber.d("URL: %s", apiUrl.toString());
Request request = new Request.Builder().get().url(apiUrl).build();
Response response = okHttpClient.newCall(request).execute();
ResponseBody body = response.body();
if (body == null) {
return Collections.emptyList();
}
ApiResponse apiResponse = gson.fromJson(body.charStream(), ApiResponse.class);
Set<String> categories = new LinkedHashSet<>();
if (apiResponse != null && apiResponse.hasPages()) {
for (Page page : apiResponse.query.pages) {
for (PageCategory category : page.getCategories()) {
categories.add(category.withoutPrefix());
}
}
}
return new ArrayList<>(categories);
});
}
/**
* Builds URL with image coords for MediaWiki API calls
* Example URL: https://commons.wikimedia.org/w/api.php?action=query&prop=categories|coordinates|pageprops&format=json&clshow=!hidden&coprop=type|name|dim|country|region|globe&codistancefrompoint=38.11386944444445|13.356263888888888&generator=geosearch&redirects=&ggscoord=38.11386944444445|1.356263888888888&ggsradius=100&ggslimit=10&ggsnamespace=6&ggsprop=type|name|dim|country|region|globe&ggsprimary=all&formatversion=2
*
* @param coords Coordinates to build query with
* @return URL for API query
*/
private HttpUrl buildUrl(String coords) {
return mwUrl.newBuilder()
.addPathSegment("w")
.addPathSegment("api.php")
.addQueryParameter("action", "query")
.addQueryParameter("prop", "categories|coordinates|pageprops")
.addQueryParameter("format", "json")
.addQueryParameter("clshow", "!hidden")
.addQueryParameter("coprop", "type|name|dim|country|region|globe")
.addQueryParameter("codistancefrompoint", coords)
.addQueryParameter("generator", "geosearch")
.addQueryParameter("ggscoord", coords)
.addQueryParameter("ggsradius", "10000")
.addQueryParameter("ggslimit", "10")
.addQueryParameter("ggsnamespace", "6")
.addQueryParameter("ggsprop", "type|name|dim|country|region|globe")
.addQueryParameter("ggsprimary", "all")
.addQueryParameter("formatversion", "2")
.build();
}
}

View file

@ -3,6 +3,8 @@ package fr.free.nrw.commons.mwapi;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
@ -27,6 +29,10 @@ public interface MediaWikiApi {
String getEditToken() throws IOException;
String getWikidataCsrfToken() throws IOException;
String getCentralAuthToken() throws IOException;
boolean fileExistsWithName(String fileName) throws IOException;
boolean pageExists(String pageName) throws IOException;
@ -37,6 +43,16 @@ public interface MediaWikiApi {
List<Media> getCategoryImages(String categoryName);
List<String> getSubCategoryList(String categoryName);
List<String> getParentCategoryList(String categoryName);
@NonNull
List<Media> searchImages(String title, int offset);
@NonNull
List<String> searchCategory(String title, int offset);
@NonNull
UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener) throws IOException;
@ -49,6 +65,12 @@ public interface MediaWikiApi {
@Nullable
String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException;
@Nullable
String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException;
@Nullable
boolean addWikidataEditTag(String revisionId) throws IOException;
@NonNull
MediaResult fetchMediaByFilename(String filename) throws IOException;
@ -75,6 +97,14 @@ public interface MediaWikiApi {
@NonNull
Single<Integer> getUploadCount(String userName);
boolean isUserBlockedFromCommons();
@NonNull
Single<JSONObject> getAchievements(String userName);
@NonNull
Single<JSONObject> getRevertCount(String userName);
interface ProgressListener {
void onProgress(long transferred, long total);
}

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.mwapi.model;
public class ApiResponse {
public Query query;
public ApiResponse() {
}
public boolean hasPages() {
return query != null && query.pages != null;
}
}

View file

@ -0,0 +1,17 @@
package fr.free.nrw.commons.mwapi.model;
import android.support.annotation.NonNull;
public class Page {
public String title;
public PageCategory[] categories;
public PageCategory category;
public Page() {
}
@NonNull
public PageCategory[] getCategories() {
return categories != null ? categories : new PageCategory[0];
}
}

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.mwapi.model;
public class PageCategory {
public String title;
public PageCategory() {
}
public String withoutPrefix() {
return title != null ? title.replace("Category:", "") : "";
}
}

View file

@ -0,0 +1,10 @@
package fr.free.nrw.commons.mwapi.model;
public class Query {
public Page[] pages;
public Query() {
pages = new Page[0];
}
}

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons.nearby;
import android.content.SharedPreferences;
import android.os.Build;
import android.support.v4.app.Fragment;
import android.support.v4.content.ContextCompat;

View file

@ -38,11 +38,13 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.UriSerializer;
import fr.free.nrw.commons.utils.ViewUtil;
import fr.free.nrw.commons.wikidata.WikidataEditListener;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
@ -51,8 +53,12 @@ import timber.log.Timber;
import uk.co.deanwild.materialshowcaseview.IShowcaseListener;
import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.*;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED;
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener {
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener,
WikidataEditListener.WikidataP18EditListener {
private static final int LOCATION_REQUEST = 1;
@ -72,6 +78,8 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
LocationServiceManager locationManager;
@Inject
NearbyController nearbyController;
@Inject WikidataEditListener wikidataEditListener;
@Inject
@Named("application_preferences") SharedPreferences applicationPrefs;
private LatLng curLatLng;
@ -106,6 +114,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
initBottomSheetBehaviour();
initDrawer();
wikidataEditListener.setAuthenticationStateListener(this);
}
private void resumeFragment() {
@ -215,7 +224,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
//Still need to check if GPS is enabled
checkGps();
lastKnownLocation = locationManager.getLKL();
refreshView(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED);
refreshView(PERMISSION_JUST_GRANTED);
} else {
//If permission not granted, go to page that says Nearby Places cannot be displayed
hideProgressBar();
@ -275,7 +284,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
private void checkLocationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (locationManager.isLocationPermissionGranted()) {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
} else {
// Should we show an explanation?
if (locationManager.isPermissionExplanationRequired(this)) {
@ -301,7 +310,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
}
}
} else {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
}
@ -310,7 +319,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 1) {
Timber.d("User is back from Settings page");
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
}
@ -318,7 +327,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
protected void onStart() {
super.onStart();
locationManager.addLocationListener(this);
locationManager.registerLocationManager();
registerLocationUpdates();
}
@Override
@ -369,8 +378,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
@Override
public void onReceive(Context context, Intent intent) {
if (NetworkUtils.isInternetConnectionEstablished(NearbyActivity.this)) {
refreshView(LocationServiceManager
.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
} else {
ViewUtil.showLongToast(NearbyActivity.this, getString(R.string.no_internet));
}
@ -386,7 +394,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
*
* @param locationChangeType defines if location shanged significantly or slightly
*/
private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) {
private void refreshView(LocationChangeType locationChangeType) {
if (lockNearbyView) {
return;
}
@ -396,15 +404,16 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
return;
}
locationManager.registerLocationManager();
registerLocationUpdates();
LatLng lastLocation = locationManager.getLastLocation();
if (curLatLng != null && curLatLng.equals(lastLocation)) { //refresh view only if location has changed
if (curLatLng != null && curLatLng.equals(lastLocation)
&& !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed
return;
}
curLatLng = lastLocation;
if (locationChangeType.equals(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED)) {
if (locationChangeType.equals(PERMISSION_JUST_GRANTED)) {
curLatLng = lastKnownLocation;
}
@ -413,8 +422,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
return;
}
if (locationChangeType.equals(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)
|| locationChangeType.equals(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED)) {
if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)
|| locationChangeType.equals(PERMISSION_JUST_GRANTED)
|| locationChangeType.equals(MAP_UPDATED)) {
progressBar.setVisibility(View.VISIBLE);
//TODO: This hack inserts curLatLng before populatePlaces is called (see #1440). Ideally a proper fix should be found
@ -429,8 +439,14 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
.loadAttractionsFromLocation(curLatLng))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces);
} else if (locationChangeType.equals(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) {
.subscribe(this::populatePlaces,
throwable -> {
Timber.d(throwable);
showErrorMessage(getString(R.string.error_fetching_nearby_places));
progressBar.setVisibility(View.GONE);
});
} else if (locationChangeType
.equals(LOCATION_SLIGHTLY_CHANGED)) {
Gson gson = new GsonBuilder()
.registerTypeAdapter(Uri.class, new UriSerializer())
.create();
@ -440,6 +456,39 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
}
}
/**
* This method first checks if the location permissions has been granted and then register the location manager for updates.
*/
private void registerLocationUpdates() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (locationManager.isLocationPermissionGranted()) {
locationManager.registerLocationManager();
} else {
// Should we show an explanation?
if (locationManager.isPermissionExplanationRequired(this)) {
new AlertDialog.Builder(this)
.setMessage(getString(R.string.location_permission_rationale_nearby))
.setPositiveButton("OK", (dialog, which) -> {
requestLocationPermissions();
dialog.dismiss();
})
.setNegativeButton("Cancel", (dialog, id) -> {
showLocationPermissionDeniedErrorDialog();
dialog.cancel();
})
.create()
.show();
} else {
// No explanation needed, we can request the permission.
requestLocationPermissions();
}
}
} else {
locationManager.registerLocationManager();
}
}
private void populatePlaces(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
List<Place> placeList = nearbyPlacesInfo.placeList;
LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates;
@ -453,7 +502,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
if (placeList.size() == 0) {
ViewUtil.showSnackbar(findViewById(R.id.container), R.string.no_nearby);
}
bundle.putString("PlaceList", gsonPlaceList);
//bundle.putString("CurLatLng", gsonCurLatLng);
bundle.putString("BoundaryCoord", gsonBoundaryCoordinates);
@ -520,7 +569,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
locationManager.removeLocationListener(this);
} else {
lockNearbyView = false;
locationManager.registerLocationManager();
registerLocationUpdates();
locationManager.addLocationListener(this);
}
}
@ -582,7 +631,12 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
.loadAttractionsFromLocation(curLatLng))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::populatePlaces);
.subscribe(this::populatePlaces,
throwable -> {
Timber.d(throwable);
showErrorMessage(getString(R.string.error_fetching_nearby_places));
progressBar.setVisibility(View.GONE);
});
nearbyMapFragment.setBundleForUpdtes(bundle);
nearbyMapFragment.updateMapSignificantly();
updateListFragment();
@ -637,16 +691,24 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
@Override
public void onLocationChangedSignificantly(LatLng latLng) {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED);
refreshView(LOCATION_SIGNIFICANTLY_CHANGED);
}
@Override
public void onLocationChangedSlightly(LatLng latLng) {
refreshView(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED);
refreshView(LOCATION_SLIGHTLY_CHANGED);
}
public void prepareViewsForSheetPosition(int bottomSheetState) {
// TODO
}
private void showErrorMessage(String message) {
ViewUtil.showLongToast(NearbyActivity.this, message);
}
@Override
public void onWikidataEditSuccessful() {
refreshView(MAP_UPDATED);
}
}

View file

@ -7,6 +7,7 @@ import android.support.graphics.drawable.VectorDrawableCompat;
import com.mapbox.mapboxsdk.annotations.IconFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -44,7 +45,7 @@ public class NearbyController {
* @return NearbyPlacesInfo a variable holds Place list without distance information
* and boundary coordinates of current Place List
*/
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) {
public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) throws IOException {
Timber.d("Loading attractions near %s", curLatLng);
NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo();

View file

@ -35,6 +35,7 @@ import timber.log.Timber;
import static android.app.Activity.RESULT_OK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
public class NearbyListFragment extends DaggerFragment {
@ -52,6 +53,11 @@ public class NearbyListFragment extends DaggerFragment {
private RecyclerView recyclerView;
private ContributionController controller;
@Inject
@Named("direct_nearby_upload_prefs")
SharedPreferences directPrefs;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -141,7 +147,7 @@ public class NearbyListFragment extends DaggerFragment {
if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);
controller.handleImagePicked(requestCode, data, true);
controller.handleImagePicked(requestCode, data, true, directPrefs.getString(WIKIDATA_ENTITY_ID_PREF, null));
} else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);

View file

@ -58,9 +58,7 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.UriDeserializer;
import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber;
@ -68,7 +66,7 @@ import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView;
import static android.app.Activity.RESULT_OK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static fr.free.nrw.commons.theme.NavigationBaseActivity.startActivityWithFlags;
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
public class NearbyMapFragment extends DaggerFragment {
@ -750,7 +748,7 @@ public class NearbyMapFragment extends DaggerFragment {
fabCamera.setOnClickListener(view -> {
if (fabCamera.isShown()) {
Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
Timber.d("Camera button tapped. Place: %s", place.toString());
storeSharedPrefs();
directUpload.initiateCameraUpload();
}
@ -758,7 +756,7 @@ public class NearbyMapFragment extends DaggerFragment {
fabGallery.setOnClickListener(view -> {
if (fabGallery.isShown()) {
Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription());
Timber.d("Gallery button tapped. Place: %s", place.toString());
storeSharedPrefs();
directUpload.initiateGalleryUpload();
}
@ -770,6 +768,7 @@ public class NearbyMapFragment extends DaggerFragment {
editor.putString("Title", place.getName());
editor.putString("Desc", place.getLongDescription());
editor.putString("Category", place.getCategory());
editor.putString(WIKIDATA_ENTITY_ID_PREF, place.getWikiDataEntityId());
editor.apply();
}
@ -805,7 +804,7 @@ public class NearbyMapFragment extends DaggerFragment {
if (resultCode == RESULT_OK) {
Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);
controller.handleImagePicked(requestCode, data, true);
controller.handleImagePicked(requestCode, data, true, directPrefs.getString(WIKIDATA_ENTITY_ID_PREF, null));
} else {
Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s",
requestCode, resultCode, data);

View file

@ -17,7 +17,7 @@ import java.util.regex.Pattern;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.utils.FileUtils;
import fr.free.nrw.commons.upload.FileUtils;
import timber.log.Timber;
public class NearbyPlaces {
@ -40,10 +40,9 @@ public class NearbyPlaces {
}
}
List<Place> getFromWikidataQuery(LatLng curLatLng, String lang) {
List<Place> getFromWikidataQuery(LatLng curLatLng, String lang) throws IOException {
List<Place> places = Collections.emptyList();
try {
// increase the radius gradually to find a satisfactory number of nearby places
while (radius <= MAX_RADIUS) {
places = getFromWikidataQuery(curLatLng, lang, radius);
@ -54,13 +53,6 @@ public class NearbyPlaces {
radius *= RADIUS_MULTIPLIER;
}
}
} catch (IOException e) {
Timber.d(e.toString());
// errors tend to be caused by too many results (and time out)
// try a small radius next time
Timber.d("back to initial radius: %f", radius);
radius = INITIAL_RADIUS;
}
// make sure we will be able to send at least one request next time
if (radius > MAX_RADIUS) {
radius = MAX_RADIUS;

View file

@ -3,12 +3,14 @@ package fr.free.nrw.commons.nearby;
import android.graphics.Bitmap;
import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import timber.log.Timber;
public class Place {
@ -50,6 +52,22 @@ public class Place {
this.distance = distance;
}
/**
* Extracts the entity id from the wikidata link
* @return returns the entity id if wikidata link exists
*/
@Nullable
public String getWikiDataEntityId() {
if (!hasWikidataLink()) {
Timber.d("Wikidata entity ID is null for place with sitelink %s", siteLinks.toString());
return null;
}
String wikiDataLink = siteLinks.getWikidataLink().toString();
Timber.d("Wikidata entity is %s", wikiDataLink);
return wikiDataLink.replace("http://www.wikidata.org/entity/", "");
}
public boolean hasWikipediaLink() {
return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getWikipediaLink()));
}
@ -79,7 +97,18 @@ public class Place {
@Override
public String toString() {
return String.format("Place(%s@%s)", name, location);
return "Place{" +
"name='" + name + '\'' +
", label='" + label + '\'' +
", longDescription='" + longDescription + '\'' +
", secondaryImageUrl='" + secondaryImageUrl + '\'' +
", location='" + location + '\'' +
", category='" + category + '\'' +
", image='" + image + '\'' +
", secondaryImage=" + secondaryImage +
", distance='" + distance + '\'' +
", siteLinks='" + siteLinks.toString() + '\'' +
'}';
}
/**

View file

@ -26,7 +26,6 @@ import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.LoginActivity;

View file

@ -58,6 +58,15 @@ public class Sitelinks implements Parcelable {
return Uri.parse(sanitisedStringUrl);
}
@Override
public String toString() {
return "Sitelinks{" +
"wikipediaLink='" + wikipediaLink + '\'' +
", commonsLink='" + commonsLink + '\'' +
", wikidataLink='" + wikidataLink + '\'' +
'}';
}
private Sitelinks(Sitelinks.Builder builder) {
this.wikidataLink = builder.wikidataLink;
this.wikipediaLink = builder.wikipediaLink;

View file

@ -16,7 +16,6 @@ import android.widget.RelativeLayout;
import com.pedrogomez.renderers.RVRendererAdapter;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.List;
@ -26,6 +25,7 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
@ -46,6 +46,8 @@ public class NotificationActivity extends NavigationBaseActivity {
@BindView(R.id.container) RelativeLayout relativeLayout;
@Inject NotificationController controller;
@Inject
MediaWikiApi mediaWikiApi;
private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment";
private NotificationWorkerFragment mNotificationWorkerFragment;
@ -81,7 +83,6 @@ public class NotificationActivity extends NavigationBaseActivity {
}
}
@SuppressLint("CheckResult")
private void addNotifications() {
Timber.d("Add notifications");

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.notification;
import android.util.Log;
import android.graphics.drawable.PictureDrawable;
import android.text.Html;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -8,17 +9,23 @@ import android.widget.ImageView;
import android.widget.TextView;
import com.borjabravo.readmoretextview.ReadMoreTextView;
import com.bumptech.glide.RequestBuilder;
import com.pedrogomez.renderers.Renderer;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.glide.SvgSoftwareLayerSetter;
import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade;
/**
* Created by root on 19.12.2017.
*/
public class NotificationRenderer extends Renderer<Notification> {
private RequestBuilder<PictureDrawable> requestBuilder;
@BindView(R.id.title) ReadMoreTextView title;
@BindView(R.id.time) TextView time;
@BindView(R.id.icon) ImageView icon;
@ -41,23 +48,32 @@ public class NotificationRenderer extends Renderer<Notification> {
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false);
ButterKnife.bind(this, inflatedView);
requestBuilder = GlideApp.with(inflatedView.getContext())
.as(PictureDrawable.class)
.error(R.drawable.round_icon_unknown)
.transition(withCrossFade())
.listener(new SvgSoftwareLayerSetter());
return inflatedView;
}
@Override
public void render() {
Notification notification = getContent();
String str = notification.notificationText.trim();
str = str.concat(" ");
title.setText(str);
setTitle(notification.notificationText);
time.setText(notification.date);
switch (notification.notificationType) {
case THANK_YOU_EDIT:
icon.setImageResource(R.drawable.ic_edit_black_24dp);
break;
default:
icon.setImageResource(R.drawable.round_icon_unknown);
}
requestBuilder.load(notification.iconUrl).into(icon);
}
/**
* Cleans up the notification text and sets it as the title
* Clean up is required to fix escaped HTML string and extra white spaces at the beginning of the notification
* @param notificationText
*/
private void setTitle(String notificationText) {
notificationText = notificationText.trim().replaceAll("(^\\h*)|(\\h*$)", "");
notificationText = Html.fromHtml(notificationText).toString();
notificationText = notificationText.concat(" ");
title.setText(notificationText);
}
public interface NotificationClicked{

View file

@ -16,12 +16,13 @@ import javax.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import static fr.free.nrw.commons.notification.NotificationType.THANK_YOU_EDIT;
import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN;
public class NotificationUtils {
private static final String COMMONS_WIKI = "commonswiki";
private static final String WIKIDATA_WIKI = "wikidatawiki";
private static final String WIKIPEDIA_WIKI = "enwiki";
public static boolean isCommonsNotification(Node document) {
if (document == null || !document.hasAttributes()) {
@ -31,6 +32,32 @@ public class NotificationUtils {
return COMMONS_WIKI.equals(element.getAttribute("wiki"));
}
/**
* Returns true if the wiki attribute corresponds to wikidatawiki
* @param document
* @return
*/
public static boolean isWikidataNotification(Node document) {
if (document == null || !document.hasAttributes()) {
return false;
}
Element element = (Element) document;
return WIKIDATA_WIKI.equals(element.getAttribute("wiki"));
}
/**
* Returns true if the wiki attribute corresponds to enwiki
* @param document
* @return
*/
public static boolean isWikipediaNotification(Node document) {
if (document == null || !document.hasAttributes()) {
return false;
}
Element element = (Element) document;
return WIKIPEDIA_WIKI.equals(element.getAttribute("wiki"));
}
public static NotificationType getNotificationType(Node document) {
Element element = (Element) document;
String type = element.getAttribute("type");
@ -68,10 +95,17 @@ public class NotificationUtils {
return notifications;
}
/**
* Currently the app is interested in showing notifications just from the following three wikis: commons, wikidata, wikipedia
* This function returns true only if the notification belongs to any of the above wikis and is of a known notification type
* @param node
* @return
*/
private static boolean isUsefulNotification(Node node) {
return isCommonsNotification(node)
&& !getNotificationType(node).equals(UNKNOWN)
&& !getNotificationType(node).equals(THANK_YOU_EDIT);
return (isCommonsNotification(node)
|| isWikidataNotification(node)
|| isWikipediaNotification(node))
&& !getNotificationType(node).equals(UNKNOWN);
}
public static boolean isBundledNotification(Node document) {
@ -97,7 +131,7 @@ public class NotificationUtils {
switch (type) {
case THANK_YOU_EDIT:
notificationText = context.getString(R.string.notifications_thank_you_edit);
notificationText = getThankYouEditDescription(document);
break;
case EDIT_USER_TALK:
notificationText = getNotificationText(document);
@ -146,6 +180,16 @@ public class NotificationUtils {
return body != null ? body.getTextContent() : "";
}
/**
* Gets the header node returned in the XML document to form the description for thank you edits
* @param document
* @return
*/
private static String getThankYouEditDescription(Node document) {
Node body = getNode(getModel(document), "header");
return body != null ? body.getTextContent() : "";
}
private static String getNotificationIconUrl(Node document) {
String format = "%s%s";
Node iconUrl = getNode(getModel(document), "iconUrl");

View file

@ -0,0 +1,35 @@
package fr.free.nrw.commons.notification;
import android.content.Context;
import android.graphics.drawable.PictureDrawable;
import android.support.annotation.NonNull;
import com.bumptech.glide.Glide;
import com.bumptech.glide.Registry;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.module.AppGlideModule;
import com.caverock.androidsvg.SVG;
import java.io.InputStream;
import fr.free.nrw.commons.glide.SvgDecoder;
import fr.free.nrw.commons.glide.SvgDrawableTranscoder;
/**
* Module for the SVG sample app.
*/
@GlideModule
public class SvgModule extends AppGlideModule {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide,
@NonNull Registry registry) {
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
.append(InputStream.class, SVG.class, new SvgDecoder());
}
// Disable manifest parsing to avoid adding similar modules twice.
@Override
public boolean isManifestParsingEnabled() {
return false;
}
}

View file

@ -3,13 +3,10 @@ package fr.free.nrw.commons.settings;
import android.Manifest;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
@ -24,8 +21,6 @@ import android.support.v4.content.FileProvider;
import android.widget.Toast;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
@ -35,7 +30,7 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.utils.FileUtils;
import fr.free.nrw.commons.upload.FileUtils;
public class SettingsFragment extends PreferenceFragment {

View file

@ -17,6 +17,7 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
@ -29,6 +30,7 @@ import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.achievements.AchievementsActivity;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
@ -67,13 +69,17 @@ public abstract class NavigationBaseActivity extends BaseActivity
setDrawerPaneWidth();
setUserName();
Menu nav_Menu = navigationView.getMenu();
View headerLayout = navigationView.getHeaderView(0);
ImageView userIcon = headerLayout.findViewById(R.id.user_icon);
if (prefs.getBoolean("login_skipped", true)) {
userIcon.setVisibility(View.GONE);
nav_Menu.findItem(R.id.action_login).setVisible(true);
nav_Menu.findItem(R.id.action_home).setVisible(false);
nav_Menu.findItem(R.id.action_notifications).setVisible(false);
nav_Menu.findItem(R.id.action_settings).setVisible(false);
nav_Menu.findItem(R.id.action_logout).setVisible(false);
}else {
userIcon.setVisibility(View.VISIBLE);
nav_Menu.findItem(R.id.action_login).setVisible(false);
nav_Menu.findItem(R.id.action_home).setVisible(true);
nav_Menu.findItem(R.id.action_notifications).setVisible(true);
@ -89,12 +95,19 @@ public abstract class NavigationBaseActivity extends BaseActivity
View navHeaderView = navigationView.getHeaderView(0);
TextView username = navHeaderView.findViewById(R.id.username);
AccountManager accountManager = AccountManager.get(this);
Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.ACCOUNT_TYPE);
if (allAccounts.length != 0) {
username.setText(allAccounts[0].name);
}
ImageView userIcon = navHeaderView.findViewById(R.id.user_icon);
userIcon.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
drawerLayout.closeDrawer(navigationView);
AchievementsActivity.startYourself(NavigationBaseActivity.this);
}
});
}
public void initBackButton() {
@ -103,6 +116,15 @@ public abstract class NavigationBaseActivity extends BaseActivity
toggle.setToolbarNavigationClickListener(v -> onBackPressed());
}
/**
* This method changes the toolbar icon to back regardless of any conditions that
* there is any fragment in the backStack or not
*/
public void forceInitBackButton() {
toggle.setDrawerIndicatorEnabled(false);
toggle.setToolbarNavigationClickListener(v -> onBackPressed());
}
public void initBack() {
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@ -175,8 +197,8 @@ public abstract class NavigationBaseActivity extends BaseActivity
.setCancelable(false)
.setPositiveButton(R.string.yes, (dialog, which) -> {
BaseLogoutListener logoutListener = new BaseLogoutListener();
// CommonsApplication app = (CommonsApplication) getApplication();
// app.clearApplicationData(this, logoutListener);
CommonsApplication app = (CommonsApplication) getApplication();
app.clearApplicationData(this, logoutListener);
})
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
.show();
@ -185,9 +207,9 @@ public abstract class NavigationBaseActivity extends BaseActivity
drawerLayout.closeDrawer(navigationView);
NotificationActivity.startYourself(this);
return true;
case R.id.action_featured_images:
case R.id.action_explore:
drawerLayout.closeDrawer(navigationView);
CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_featured_images), FEATURED_IMAGES_CATEGORY);
CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_explore), FEATURED_IMAGES_CATEGORY);
return true;
default:
Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
@ -215,4 +237,16 @@ public abstract class NavigationBaseActivity extends BaseActivity
}
context.startActivity(intent);
}
/**
* Handles visibility of navigation base toolbar
* @param show : Used to handle visibility of toolbar
*/
public void setNavigationBaseToolbarVisibility(boolean show){
if (show){
toolbar.setVisibility(View.VISIBLE);
}else {
toolbar.setVisibility(View.GONE);
}
}
}

View file

@ -0,0 +1,37 @@
package fr.free.nrw.commons.ui.LongTitlePreferences;
import android.content.Context;
import android.preference.EditTextPreference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* Created by seannemann on 6/27/2018.
*/
public class LongTitleEditTextPreference extends EditTextPreference {
public LongTitleEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public LongTitleEditTextPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LongTitleEditTextPreference(Context context) {
super(context);
}
@Override
protected void onBindView(View view)
{
super.onBindView(view);
TextView title= view.findViewById(android.R.id.title);
if (title != null) {
title.setSingleLine(false);
}
}
}

View file

@ -0,0 +1,32 @@
package fr.free.nrw.commons.ui.LongTitlePreferences;
import android.content.Context;
import android.preference.ListPreference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* Created by seannemann on 6/27/2018.
*/
public class LongTitleListPreference extends ListPreference {
public LongTitleListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LongTitleListPreference(Context context) {
super(context);
}
@Override
protected void onBindView(View view)
{
super.onBindView(view);
TextView title= view.findViewById(android.R.id.title);
if (title != null) {
title.setSingleLine(false);
}
}
}

View file

@ -0,0 +1,36 @@
package fr.free.nrw.commons.ui.LongTitlePreferences;
import android.content.Context;
import android.preference.Preference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* Created by seannemann on 6/27/2018.
*/
public class LongTitlePreference extends Preference {
public LongTitlePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public LongTitlePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LongTitlePreference(Context context) {
super(context);
}
@Override
protected void onBindView(View view)
{
super.onBindView(view);
TextView title= view.findViewById(android.R.id.title);
if (title != null) {
title.setSingleLine(false);
}
}
}

View file

@ -0,0 +1,36 @@
package fr.free.nrw.commons.ui.LongTitlePreferences;
import android.content.Context;
import android.preference.PreferenceCategory;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* Created by seannemann on 6/27/2018.
*/
public class LongTitlePreferenceCategory extends PreferenceCategory {
public LongTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public LongTitlePreferenceCategory(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LongTitlePreferenceCategory(Context context) {
super(context);
}
@Override
protected void onBindView(View view)
{
super.onBindView(view);
TextView title= view.findViewById(android.R.id.title);
if (title != null) {
title.setSingleLine(false);
}
}
}

View file

@ -0,0 +1,36 @@
package fr.free.nrw.commons.ui.LongTitlePreferences;
import android.content.Context;
import android.preference.SwitchPreference;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
/**
* Created by seannemann on 6/27/2018.
*/
public class LongTitleSwitchPreference extends SwitchPreference {
public LongTitleSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public LongTitleSwitchPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LongTitleSwitchPreference(Context context) {
super(context);
}
@Override
protected void onBindView(View view)
{
super.onBindView(view);
TextView title= view.findViewById(android.R.id.title);
if (title != null) {
title.setSingleLine(false);
}
}
}

View file

@ -1,10 +1,8 @@
package fr.free.nrw.commons.upload;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.BitmapRegionDecoder;
import android.net.Uri;
import android.os.AsyncTask;
import android.support.v7.app.AlertDialog;

View file

@ -0,0 +1,262 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.caching.CacheController;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.CategoryApi;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext;
/**
* Processing of the image file that is about to be uploaded via ShareActivity is done here
*/
public class FileProcessor implements SimilarImageDialogFragment.onResponse {
@Inject
CacheController cacheController;
@Inject
GpsCategoryModel gpsCategoryModel;
@Inject
CategoryApi apiCall;
@Inject
@Named("default_preferences")
SharedPreferences prefs;
private Uri mediaUri;
private ContentResolver contentResolver;
private GPSExtractor imageObj;
private Context context;
private String decimalCoords;
private boolean haveCheckedForOtherImages = false;
private String filePath;
private boolean useExtStorage;
private boolean cacheFound;
private GPSExtractor tempImageObj;
FileProcessor(Uri mediaUri, ContentResolver contentResolver, Context context) {
this.mediaUri = mediaUri;
this.contentResolver = contentResolver;
this.context = context;
ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this);
useExtStorage = prefs.getBoolean("useExternalStorage", true);
}
/**
* Gets file path from media URI.
* In older devices getPath() may fail depending on the source URI, creating and using a copy of the file seems to work instead.
*
* @return file path of media
*/
@Nullable
private String getPathOfMediaOrCopy() {
filePath = FileUtils.getPath(context, mediaUri);
Timber.d("Filepath: " + filePath);
if (filePath == null) {
String copyPath = null;
try {
ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r");
if (descriptor != null) {
if (useExtStorage) {
copyPath = FileUtils.createCopyPath(descriptor);
return copyPath;
}
copyPath = getApplicationContext().getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + ".jpg";
FileUtils.copy(descriptor.getFileDescriptor(), copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
}
} catch (IOException e) {
Timber.w(e, "Error in file " + copyPath);
return null;
}
}
return filePath;
}
/**
* Processes file coordinates, either from EXIF data or user location
*
* @param gpsEnabled if true use GPS
*/
GPSExtractor processFileCoordinates(boolean gpsEnabled) {
Timber.d("Calling GPSExtractor");
try {
ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (descriptor != null) {
imageObj = new GPSExtractor(descriptor.getFileDescriptor());
}
} else {
String filePath = getPathOfMediaOrCopy();
if (filePath != null) {
imageObj = new GPSExtractor(filePath);
}
}
decimalCoords = imageObj.getCoords();
if (decimalCoords == null || !imageObj.imageCoordsExists) {
//Find other photos taken around the same time which has gps coordinates
if (!haveCheckedForOtherImages)
findOtherImages();// Do not do repeat the process
} else {
useImageCoords();
}
} catch (FileNotFoundException e) {
Timber.w("File not found: " + mediaUri, e);
}
return imageObj;
}
String getDecimalCoords() {
return decimalCoords;
}
/**
* Find other images around the same location that were taken within the last 20 sec
*
*/
private void findOtherImages() {
Timber.d("filePath" + getPathOfMediaOrCopy());
long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created
File folder = new File(filePath.substring(0, filePath.lastIndexOf('/')));
File[] files = folder.listFiles();
Timber.d("folderTime Number:" + files.length);
for (File file : files) {
if (file.lastModified() - timeOfCreation <= (120 * 1000) && file.lastModified() - timeOfCreation >= -(120 * 1000)) {
//Make sure the photos were taken within 20seconds
Timber.d("fild date:" + file.lastModified() + " time of creation" + timeOfCreation);
tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos
ParcelFileDescriptor descriptor = null;
try {
descriptor = contentResolver.openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
if (descriptor != null) {
tempImageObj = new GPSExtractor(descriptor.getFileDescriptor());
}
} else {
if (filePath != null) {
tempImageObj = new GPSExtractor(file.getAbsolutePath());
}
}
if (tempImageObj != null) {
Timber.d("not null fild EXIF" + tempImageObj.imageCoordsExists + " coords" + tempImageObj.getCoords());
if (tempImageObj.getCoords() != null && tempImageObj.imageCoordsExists) {
// Current image has gps coordinates and it's not current gps locaiton
Timber.d("This file has image coords:" + file.getAbsolutePath());
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
Bundle args = new Bundle();
args.putString("originalImagePath", filePath);
args.putString("possibleImagePath", file.getAbsolutePath());
newFragment.setArguments(args);
newFragment.show(((AppCompatActivity) context).getSupportFragmentManager(), "dialog");
break;
}
}
}
}
haveCheckedForOtherImages = true; //Finished checking for other images
}
/**
* Initiates retrieval of image coordinates or user coordinates, and caching of coordinates.
* Then initiates the calls to MediaWiki API through an instance of CategoryApi.
*/
@SuppressLint("CheckResult")
public void useImageCoords() {
if (decimalCoords != null) {
Timber.d("Decimal coords of image: %s", decimalCoords);
Timber.d("is EXIF data present:" + imageObj.imageCoordsExists + " from findOther image");
// Only set cache for this point if image has coords
if (imageObj.imageCoordsExists) {
double decLongitude = imageObj.getDecLongitude();
double decLatitude = imageObj.getDecLatitude();
cacheController.setQtPoint(decLongitude, decLatitude);
}
List<String> displayCatList = cacheController.findCategory();
boolean catListEmpty = displayCatList.isEmpty();
// If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories
if (catListEmpty) {
cacheFound = false;
apiCall.request(decimalCoords)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(
gpsCategoryModel::setCategoryList,
throwable -> {
Timber.e(throwable);
gpsCategoryModel.clear();
}
);
Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList);
} else {
cacheFound = true;
Timber.d("Cache found, setting categoryList in model to %s", displayCatList);
gpsCategoryModel.setCategoryList(displayCatList);
}
} else {
Timber.d("EXIF: no coords");
}
}
boolean isCacheFound() {
return cacheFound;
}
/**
* Calls the async task that detects if image is fuzzy, too dark, etc
*/
void detectUnwantedPictures() {
String imageMediaFilePath = FileUtils.getPath(context, mediaUri);
DetectUnwantedPicturesAsync detectUnwantedPicturesAsync
= new DetectUnwantedPicturesAsync(new WeakReference<Activity>((Activity) context), imageMediaFilePath);
detectUnwantedPicturesAsync.execute();
}
@Override
public void onPositiveResponse() {
imageObj = tempImageObj;
decimalCoords = imageObj.getCoords();// Not necessary to use gps as image already ha EXIF data
Timber.d("EXIF from tempImageObj");
useImageCoords();
}
@Override
public void onNegativeResponse() {
Timber.d("EXIF from imageObj");
useImageCoords();
}
}

View file

@ -15,18 +15,84 @@ import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.math.BigInteger;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import timber.log.Timber;
public class FileUtils {
/**
* Get SHA1 of file from input stream
*/
static String getSHA1(InputStream is) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
Timber.e(e, "Exception while getting Digest");
return "";
}
byte[] buffer = new byte[8192];
int read;
try {
while ((read = is.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
byte[] md5sum = digest.digest();
BigInteger bigInt = new BigInteger(1, md5sum);
String output = bigInt.toString(16);
// Fill to 40 chars
output = String.format("%40s", output).replace(' ', '0');
Timber.i("File SHA1: %s", output);
return output;
} catch (IOException e) {
Timber.e(e, "IO Exception");
return "";
} finally {
try {
is.close();
} catch (IOException e) {
Timber.e(e, "Exception on closing MD5 input stream");
}
}
}
/**
* In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead.
* @return path of copy
*/
@Nullable
static String createCopyPath(ParcelFileDescriptor descriptor) {
try {
String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + ".jpg";
File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
newFile.mkdir();
FileUtils.copy(descriptor.getFileDescriptor(), copyPath);
Timber.d("Filepath (copied): %s", copyPath);
return copyPath;
} catch (IOException e) {
Timber.e(e);
return null;
}
}
/**
* Get a file path from a Uri. This will get the the path for Storage Access
* Framework Documents, as well as the _data field for the MediaStore and
@ -235,4 +301,80 @@ public class FileUtils {
copy(new FileInputStream(source), new FileOutputStream(destination));
}
/**
* Read and return the content of a resource file as string.
* @param fileName asset file's path (e.g. "/queries/nearby_query.rq")
* @return the content of the file
*/
public static String readFromResource(String fileName) throws IOException {
StringBuilder buffer = new StringBuilder();
BufferedReader reader = null;
try {
InputStream inputStream = FileUtils.class.getResourceAsStream(fileName);
if (inputStream == null) {
throw new FileNotFoundException(fileName);
}
reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
buffer.append(line).append("\n");
}
} finally {
if (reader != null) {
reader.close();
}
}
return buffer.toString();
}
/**
* Deletes files.
* @param file context
*/
public static boolean deleteFile(File file) {
boolean deletedAll = true;
if (file != null) {
if (file.isDirectory()) {
String[] children = file.list();
for (String child : children) {
deletedAll = deleteFile(new File(file, child)) && deletedAll;
}
} else {
deletedAll = file.delete();
}
}
return deletedAll;
}
public static File createAndGetAppLogsFile(String logs) {
try {
File commonsAppDirectory = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
if (!commonsAppDirectory.exists()) {
commonsAppDirectory.mkdir();
}
File logsFile = new File(commonsAppDirectory,"logs.txt");
if (logsFile.exists()) {
//old logs file is useless
logsFile.delete();
}
logsFile.createNewFile();
FileOutputStream outputStream = new FileOutputStream(logsFile);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
outputStreamWriter.append(logs);
outputStreamWriter.close();
outputStream.flush();
outputStream.close();
return logsFile;
} catch (IOException ioe) {
Timber.e(ioe);
return null;
}
}
}

View file

@ -1,13 +1,6 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import android.content.SharedPreferences;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.media.ExifInterface;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
@ -19,31 +12,21 @@ import timber.log.Timber;
/**
* Extracts geolocation to be passed to API for category suggestions. If a picture with geolocation
* is uploaded, extract latitude and longitude from EXIF data of image. If a picture without
* geolocation is uploaded, retrieve user's location (if enabled in Settings).
* is uploaded, extract latitude and longitude from EXIF data of image.
*/
public class GPSExtractor {
private final Context context;
private SharedPreferences prefs;
private ExifInterface exif;
private double decLatitude;
private double decLongitude;
private Double currentLatitude = null;
private Double currentLongitude = null;
public boolean imageCoordsExists;
private MyLocationListener myLocationListener;
private LocationManager locationManager;
/**
* Construct from the file descriptor of the image (only for API 24 or newer).
* @param fileDescriptor the file descriptor of the image
* @param context the context
*/
@RequiresApi(24)
public GPSExtractor(@NonNull FileDescriptor fileDescriptor, Context context, SharedPreferences prefs) {
this.context = context;
this.prefs = prefs;
public GPSExtractor(@NonNull FileDescriptor fileDescriptor) {
try {
exif = new ExifInterface(fileDescriptor);
} catch (IOException | IllegalArgumentException e) {
@ -54,65 +37,22 @@ public class GPSExtractor {
/**
* Construct from the file path of the image.
* @param path file path of the image
* @param context the context
*
*/
public GPSExtractor(@NonNull String path, Context context, SharedPreferences prefs) {
this.prefs = prefs;
public GPSExtractor(@NonNull String path) {
try {
exif = new ExifInterface(path);
} catch (IOException | IllegalArgumentException e) {
Timber.w(e);
}
this.context = context;
}
/**
* Check if user enabled retrieval of their current location in Settings
* @return true if enabled, false if disabled
*/
private boolean gpsPreferenceEnabled() {
boolean gpsPref = prefs.getBoolean("allowGps", false);
Timber.d("Gps pref set to: %b", gpsPref);
return gpsPref;
}
/**
* Registers a LocationManager to listen for current location
*/
protected void registerLocationManager() {
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
Criteria criteria = new Criteria();
String provider = locationManager.getBestProvider(criteria, true);
myLocationListener = new MyLocationListener();
try {
locationManager.requestLocationUpdates(provider, 400, 1, myLocationListener);
Location location = locationManager.getLastKnownLocation(provider);
if (location != null) {
myLocationListener.onLocationChanged(location);
}
} catch (IllegalArgumentException e) {
Timber.e(e, "Illegal argument exception");
} catch (SecurityException e) {
Timber.e(e, "Security exception");
}
}
protected void unregisterLocationManager() {
try {
locationManager.removeUpdates(myLocationListener);
} catch (SecurityException e) {
Timber.e(e, "Security exception");
}
}
/**
* Extracts geolocation (either of image from EXIF data, or of user)
* @param useGPS set to true if location permissions allowed (by API 23), false if disallowed
* @return coordinates as string (needs to be passed as a String in API query)
*/
@Nullable
public String getCoords(boolean useGPS) {
public String getCoords() {
String latitude;
String longitude;
String latitudeRef;
@ -120,30 +60,9 @@ public class GPSExtractor {
String decimalCoords;
//If image has no EXIF data and user has enabled GPS setting, get user's location
//TODO: Always return null as a temporary fix for #1599
if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) {
if (useGPS) {
registerLocationManager();
imageCoordsExists = false;
Timber.d("EXIF data has no location info");
//Check what user's preference is for automatic location detection
boolean gpsPrefEnabled = gpsPreferenceEnabled();
//Check that currentLatitude and currentLongitude have been
// explicitly set by MyLocationListener
// and do not default to (0.0,0.0)
if (gpsPrefEnabled && currentLatitude != null && currentLongitude != null) {
Timber.d("Current location values: Lat = %f Long = %f",
currentLatitude, currentLongitude);
return String.valueOf(currentLatitude) + "|" + String.valueOf(currentLongitude);
} else {
// No coords found
return null;
}
} else {
return null;
}
return null;
} else {
//If image has EXIF data, extract image coords
imageCoordsExists = true;
@ -166,33 +85,6 @@ public class GPSExtractor {
}
}
/**
* Listen for user's location when it changes
*/
private class MyLocationListener implements LocationListener {
@Override
public void onLocationChanged(Location location) {
currentLatitude = location.getLatitude();
currentLongitude = location.getLongitude();
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
Timber.d("%s's status changed to %d", provider, status);
}
@Override
public void onProviderEnabled(String provider) {
Timber.d("Provider %s enabled", provider);
}
@Override
public void onProviderDisabled(String provider) {
Timber.d("Provider %s disabled", provider);
}
}
public double getDecLatitude() {
return decLatitude;
}

View file

@ -0,0 +1,40 @@
package fr.free.nrw.commons.upload;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class GpsCategoryModel {
private Set<String> categorySet;
@Inject
public GpsCategoryModel() {
clear();
}
public void clear() {
categorySet = new HashSet<>();
}
public boolean getGpsCatExists() {
return !categorySet.isEmpty();
}
public List<String> getCategoryList() {
return new ArrayList<>(categorySet);
}
public void setCategoryList(List<String> categoryList) {
clear();
categorySet.addAll(categoryList != null ? categoryList : new ArrayList<>());
}
public void add(String categoryString) {
categorySet.add(categoryString);
}
}

View file

@ -47,6 +47,8 @@ import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import timber.log.Timber;
//TODO: We should use this class to see how multiple uploads are handled, and then REMOVE it.
public class MultipleShareActivity extends AuthenticatedActivity
implements MediaDetailPagerFragment.MediaDetailProvider,
AdapterView.OnItemClickListener,
@ -327,18 +329,18 @@ public class MultipleShareActivity extends AuthenticatedActivity
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(imageUri,"r");
if (fd != null) {
gpsExtractor = new GPSExtractor(fd.getFileDescriptor(),this,prefs);
gpsExtractor = new GPSExtractor(fd.getFileDescriptor());
}
} else {
String filePath = FileUtils.getPath(this,imageUri);
if (filePath != null) {
gpsExtractor = new GPSExtractor(filePath,this,prefs);
gpsExtractor = new GPSExtractor(filePath);
}
}
if (gpsExtractor != null) {
//get image coordinates from exif data or user location
return gpsExtractor.getCoords(locationPermitted);
return gpsExtractor.getCoords();
}
} catch (FileNotFoundException fnfe) {

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons.upload;
import android.app.Activity;
import android.content.Context;
import android.graphics.Point;
import android.net.Uri;
@ -17,7 +16,6 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.EditText;
@ -26,6 +24,8 @@ import android.widget.GridView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.view.SimpleDraweeView;
@ -41,9 +41,13 @@ public class MultipleUploadListFragment extends Fragment {
void OnMultipleUploadInitiated();
}
private GridView photosGrid;
@BindView(R.id.multipleShareBackground)
GridView photosGrid;
@BindView(R.id.multipleBaseTitle)
EditText baseTitle;
private PhotoDisplayAdapter photosAdapter;
private EditText baseTitle;
private TitleTextWatcher textWatcher = new TitleTextWatcher();
private Point photoSize;
@ -166,9 +170,7 @@ public class MultipleUploadListFragment extends Fragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_multiple_uploads_list, container, false);
photosGrid = view.findViewById(R.id.multipleShareBackground);
baseTitle = view.findViewById(R.id.multipleBaseTitle);
ButterKnife.bind(this,view);
photosAdapter = new PhotoDisplayAdapter();
photosGrid.setAdapter(photosAdapter);
photosGrid.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());

View file

@ -1,249 +0,0 @@
package fr.free.nrw.commons.upload;
import android.content.Context;
import android.net.Uri;
import com.android.volley.Cache;
import com.android.volley.NetworkResponse;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HttpHeaderParser;
import com.android.volley.toolbox.JsonRequest;
import com.android.volley.toolbox.Volley;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import timber.log.Timber;
/**
* Uses the Volley library to implement asynchronous calls to the Commons MediaWiki API to match
* GPS coordinates with nearby Commons categories. Parses the results using GSON to obtain a list
* of relevant categories.
*/
public class MwVolleyApi {
private static RequestQueue REQUEST_QUEUE;
private static final Gson GSON = new GsonBuilder().create();
private static Set<String> categorySet;
private static List<String> categoryList;
private static final String MWURL = "https://commons.wikimedia.org/";
private final Context context;
public MwVolleyApi(Context context) {
this.context = context;
categorySet = new HashSet<>();
}
public static List<String> getGpsCat() {
return categoryList;
}
public static void setGpsCat(List<String> cachedList) {
categoryList = new ArrayList<>();
categoryList.addAll(cachedList);
Timber.d("Setting GPS cats from cache: %s", categoryList);
}
public void request(String coords) {
String apiUrl = buildUrl(coords);
Timber.d("URL: %s", apiUrl);
JsonRequest<QueryResponse> request = new QueryRequest(apiUrl,
new LogResponseListener<>(), new LogResponseErrorListener());
getQueue().add(request);
}
/**
* Builds URL with image coords for MediaWiki API calls
* Example URL: https://commons.wikimedia.org/w/api.php?action=query&prop=categories|coordinates|pageprops&format=json&clshow=!hidden&coprop=type|name|dim|country|region|globe&codistancefrompoint=38.11386944444445|13.356263888888888&generator=geosearch&redirects=&ggscoord=38.11386944444445|1.356263888888888&ggsradius=100&ggslimit=10&ggsnamespace=6&ggsprop=type|name|dim|country|region|globe&ggsprimary=all&formatversion=2
* @param coords Coordinates to build query with
* @return URL for API query
*/
private String buildUrl(String coords) {
Uri.Builder builder = Uri.parse(MWURL).buildUpon();
builder.appendPath("w")
.appendPath("api.php")
.appendQueryParameter("action", "query")
.appendQueryParameter("prop", "categories|coordinates|pageprops")
.appendQueryParameter("format", "json")
.appendQueryParameter("clshow", "!hidden")
.appendQueryParameter("coprop", "type|name|dim|country|region|globe")
.appendQueryParameter("codistancefrompoint", coords)
.appendQueryParameter("generator", "geosearch")
.appendQueryParameter("ggscoord", coords)
.appendQueryParameter("ggsradius", "10000")
.appendQueryParameter("ggslimit", "10")
.appendQueryParameter("ggsnamespace", "6")
.appendQueryParameter("ggsprop", "type|name|dim|country|region|globe")
.appendQueryParameter("ggsprimary", "all")
.appendQueryParameter("formatversion", "2");
return builder.toString();
}
private synchronized RequestQueue getQueue() {
if (REQUEST_QUEUE == null) {
REQUEST_QUEUE = Volley.newRequestQueue(context);
}
return REQUEST_QUEUE;
}
private static class LogResponseListener<T> implements Response.Listener<T> {
@Override
public void onResponse(T response) {
Timber.d(response.toString());
}
}
private static class LogResponseErrorListener implements Response.ErrorListener {
@Override
public void onErrorResponse(VolleyError error) {
Timber.e(error.toString());
}
}
private static class QueryRequest extends JsonRequest<QueryResponse> {
public QueryRequest(String url,
Response.Listener<QueryResponse> listener,
Response.ErrorListener errorListener) {
super(Request.Method.GET, url, null, listener, errorListener);
}
@Override
protected Response<QueryResponse> parseNetworkResponse(NetworkResponse response) {
String json = parseString(response);
QueryResponse queryResponse = GSON.fromJson(json, QueryResponse.class);
return Response.success(queryResponse, cacheEntry(response));
}
private Cache.Entry cacheEntry(NetworkResponse response) {
return HttpHeaderParser.parseCacheHeaders(response);
}
private String parseString(NetworkResponse response) {
try {
return new String(response.data, HttpHeaderParser.parseCharset(response.headers));
} catch (UnsupportedEncodingException e) {
return new String(response.data);
}
}
}
public static class GpsCatExists {
private static boolean gpsCatExists;
public static void setGpsCatExists(boolean gpsCat) {
gpsCatExists = gpsCat;
}
public static boolean getGpsCatExists() {
return gpsCatExists;
}
}
private static class QueryResponse {
private Query query = new Query();
private String printSet() {
if (categorySet == null || categorySet.isEmpty()) {
GpsCatExists.setGpsCatExists(false);
Timber.d("gpsCatExists=%b", GpsCatExists.getGpsCatExists());
return "No collection of categories";
} else {
GpsCatExists.setGpsCatExists(true);
Timber.d("gpsCatExists=%b", GpsCatExists.getGpsCatExists());
return "CATEGORIES FOUND" + categorySet.toString();
}
}
@Override
public String toString() {
if (query != null) {
return "query=" + query.toString() + "\n" + printSet();
} else {
return "No pages found";
}
}
}
private static class Query {
private Page [] pages;
@Override
public String toString() {
StringBuilder builder = new StringBuilder("pages=" + "\n");
if (pages != null) {
for (Page page : pages) {
builder.append(page.toString());
builder.append("\n");
}
builder.replace(builder.length() - 1, builder.length(), "");
return builder.toString();
} else {
return "No pages found";
}
}
}
public static class Page {
private int pageid;
private int ns;
private String title;
private Category[] categories;
private Category category;
public Page() {
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("PAGEID=" + pageid + " ns=" + ns + " title=" + title + "\n" + " CATEGORIES= ");
if (categories == null || categories.length == 0) {
builder.append("no categories exist\n");
} else {
for (Category category : categories) {
builder.append(category.toString());
builder.append("\n");
if (category != null) {
String categoryString = category.toString().replace("Category:", "");
categorySet.add(categoryString);
}
}
}
categoryList = new ArrayList<>(categorySet);
builder.replace(builder.length() - 1, builder.length(), "");
return builder.toString();
}
}
private static class Category {
private String title;
@Override
public String toString() {
return title;
}
}
}

View file

@ -13,6 +13,9 @@ import android.view.ViewGroup;
import android.view.Window;
import android.widget.Button;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.view.SimpleDraweeView;
import com.facebook.imagepipeline.listener.RequestListener;
@ -29,29 +32,33 @@ import fr.free.nrw.commons.R;
*/
public class SimilarImageDialogFragment extends DialogFragment {
@BindView(R.id.orginalImage)
SimpleDraweeView originalImage;
@BindView(R.id.possibleImage)
SimpleDraweeView possibleImage;
@BindView(R.id.postive_button)
Button positiveButton;
@BindView(R.id.negative_button)
Button negativeButton;
onResponse mOnResponse;//Implemented interface from shareActivity
Boolean gotResponse = false;
public SimilarImageDialogFragment() {
}
public interface onResponse{
public void onPostiveResponse();
public void onPositiveResponse();
public void onNegativeResponse();
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_similar_image_dialog, container, false);
ButterKnife.bind(this,view);
Set<RequestListener> requestListeners = new HashSet<>();
requestListeners.add(new RequestLoggingListener());
originalImage =(SimpleDraweeView) view.findViewById(R.id.orginalImage);
possibleImage =(SimpleDraweeView) view.findViewById(R.id.possibleImage);
positiveButton = (Button) view.findViewById(R.id.postive_button);
negativeButton = (Button) view.findViewById(R.id.negative_button);
originalImage.setHierarchy(GenericDraweeHierarchyBuilder
.newInstance(getResources())
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
@ -70,22 +77,6 @@ public class SimilarImageDialogFragment extends DialogFragment {
originalImage.setImageURI(Uri.fromFile(new File(getArguments().getString("originalImagePath"))));
possibleImage.setImageURI(Uri.fromFile(new File(getArguments().getString("possibleImagePath"))));
negativeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mOnResponse.onNegativeResponse();
gotResponse = true;
dismiss();
}
});
positiveButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mOnResponse.onPostiveResponse();
gotResponse = true;
dismiss();
}
});
return view;
}
@ -105,8 +96,23 @@ public class SimilarImageDialogFragment extends DialogFragment {
@Override
public void onDismiss(DialogInterface dialog) {
// I user dismisses dialog by pressing outside the dialog.
if(!gotResponse)
if (!gotResponse) {
mOnResponse.onNegativeResponse();
}
super.onDismiss(dialog);
}
@OnClick(R.id.negative_button)
public void onNegativeButtonClicked() {
mOnResponse.onNegativeResponse();
gotResponse = true;
dismiss();
}
@OnClick(R.id.postive_button)
public void onPositiveButtonClicked() {
mOnResponse.onPositiveResponse();
gotResponse = true;
dismiss();
}
}

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
@ -13,6 +14,7 @@ import android.text.Editable;
import android.text.Html;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@ -29,6 +31,7 @@ import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
@ -74,13 +77,13 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
//What happens when the 'submit' icon is tapped
case R.id.menu_upload_single:
if (titleEdit.getText().toString().isEmpty()) {
if (titleEdit.getText().toString().trim().isEmpty()) {
Toast.makeText(getContext(), R.string.add_title_toast, Toast.LENGTH_LONG).show();
return false;
}
String title = titleEdit.getText().toString();
String desc = descEdit.getText().toString();
String title = titleEdit.getText().toString().trim();
String desc = descEdit.getText().toString().trim();
//Save the title/desc in short-lived cache so next time this fragment is loaded, we can access these
prefs.edit()
@ -342,4 +345,17 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
.create()
.show();
}
/**
* To launch the Commons:Licensing
* @param view
*/
@OnClick(R.id.licenseInfo)
public void launchLicenseInfo(View view){
Log.i("Language", Locale.getDefault().getLanguage());
UrlLicense urlLicense = new UrlLicense();
urlLicense.initialize();
String url = urlLicense.getLicenseUrl(Locale.getDefault().getLanguage());
Utils.handleWebUrl(getActivity() , Uri.parse(url));
}
}

View file

@ -1,5 +1,7 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
import android.accounts.Account;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
@ -13,6 +15,7 @@ import android.os.AsyncTask;
import android.os.IBinder;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.widget.Toast;
import java.io.BufferedInputStream;
import java.io.IOException;
@ -22,9 +25,15 @@ import java.util.concurrent.Executors;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.HandlerService;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber;
public class UploadController {
@ -82,28 +91,50 @@ public class UploadController {
/**
* Starts a new upload task.
*
* @param title the title of the contribution
* @param mediaUri the media URI of the contribution
* @param description the description of the contribution
* @param mimeType the MIME type of the contribution
* @param source the source of the contribution
* @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615")
* @param wikiDataEntityId
* @param onComplete the progress tracker
*/
public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, ContributionUploadProgress onComplete) {
public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, String wikiDataEntityId, ContributionUploadProgress onComplete) {
Contribution contribution;
//TODO: Modify this to include coords
contribution = new Contribution(mediaUri, null, title, description, -1,
null, null, sessionManager.getCurrentAccount().name,
CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords);
contribution.setTag("mimeType", mimeType);
contribution.setSource(source);
//Calls the next overloaded method
startUpload(contribution, onComplete);
Timber.d("Wikidata entity ID received from Share activity is %s", wikiDataEntityId);
//TODO: Modify this to include coords
Account currentAccount = sessionManager.getCurrentAccount();
if(currentAccount == null) {
Timber.d("Current account is null");
ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in));
sessionManager.forceLogin(context);
return;
}
contribution = new Contribution(mediaUri, null, title, description, -1,
null, null, sessionManager.getCurrentAccount().name,
null, null, currentAccount.name,
CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords);
contribution.setTag("mimeType", mimeType);
contribution.setSource(source);
contribution.setWikiDataEntityId(wikiDataEntityId);
//Calls the next overloaded method
startUpload(contribution, onComplete);
}
/**
@ -112,6 +143,7 @@ public class UploadController {
* @param contribution the contribution object
* @param onComplete the progress tracker
*/
@SuppressLint("StaticFieldLeak")
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
//Set creator, desc, and license
if (TextUtils.isEmpty(contribution.getCreator())) {

Some files were not shown because too many files have changed in this diff Show more