diff --git a/app/build.gradle b/app/build.gradle
index 535f58143..9c6f62fd4 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -49,6 +49,8 @@ dependencies {
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'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6aab09b55..17f6770d2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -92,8 +92,9 @@
android:label="@string/navigation_item_notification" />
+ android:name=".category.CategoryImagesActivity"
+ android:label="@string/title_activity_featured_images"
+ android:parentActivityName=".contributions.ContributionsActivity" />
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java
new file mode 100644
index 000000000..3495d710c
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java
@@ -0,0 +1,29 @@
+package fr.free.nrw.commons.category;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.mwapi.MediaWikiApi;
+
+@Singleton
+public class CategoryImageController {
+
+ private MediaWikiApi mediaWikiApi;
+
+ @Inject
+ public CategoryImageController(MediaWikiApi mediaWikiApi) {
+ this.mediaWikiApi = mediaWikiApi;
+ }
+
+ /**
+ * Takes a category name as input and calls the API to get a list of images for that category
+ * @param categoryName
+ * @return
+ */
+ public List getCategoryImages(String categoryName) {
+ return mediaWikiApi.getCategoryImages(categoryName);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java
new file mode 100644
index 000000000..18749847e
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java
@@ -0,0 +1,225 @@
+package fr.free.nrw.commons.category;
+
+import org.jsoup.Jsoup;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import fr.free.nrw.commons.Media;
+import timber.log.Timber;
+
+public class CategoryImageUtils {
+
+ /**
+ * The method iterates over the child nodes to return a list of Media objects
+ * @param childNodes
+ * @return
+ */
+ public static List getMediaList(NodeList childNodes) {
+ List categoryImages = new ArrayList<>();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Node node = childNodes.item(i);
+ categoryImages.add(getMediaFromPage(node));
+ }
+
+ return categoryImages;
+ }
+
+ /**
+ * Creates a new Media object from the XML response as received by the API
+ * @param node
+ * @return
+ */
+ private static Media getMediaFromPage(Node node) {
+ Media media = new Media(null,
+ getImageUrl(node),
+ getFileName(node),
+ getDescription(node),
+ getDataLength(node),
+ getDateCreated(node),
+ getDateCreated(node),
+ getCreator(node)
+ );
+
+ media.setLicense(getLicense(node));
+
+ return media;
+ }
+
+ /**
+ * Extracts the filename of the uploaded image
+ * @param document
+ * @return
+ */
+ private static String getFileName(Node document) {
+ Element element = (Element) document;
+ return element.getAttribute("title");
+ }
+
+ /**
+ * Extracts the image description for that particular upload
+ * @param document
+ * @return
+ */
+ private static String getDescription(Node document) {
+ return getMetaDataValue(document, "ImageDescription");
+ }
+
+ /**
+ * Extracts license information from the image meta data
+ * @param document
+ * @return
+ */
+ private static String getLicense(Node document) {
+ return getMetaDataValue(document, "License");
+ }
+
+ /**
+ * Returns the parsed value of artist from the response
+ * The artist information is returned as a HTML string from the API. Jsoup library parses the HTML string
+ * to extract just the text value
+ * @param document
+ * @return
+ */
+ private static String getCreator(Node document) {
+ String artist = getMetaDataValue(document, "Artist");
+ if (artist != null) {
+ return Jsoup.parse(artist).text();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the parsed date of creation of the image
+ * @param document
+ * @return
+ */
+ private static Date getDateCreated(Node document) {
+ String dateTime = getMetaDataValue(document, "DateTime");
+ if (dateTime != null && !dateTime.equals("")) {
+ SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ try {
+ return format.parse(dateTime);
+ } catch (ParseException e) {
+ Timber.d("Error occurred while parsing date %s", dateTime);
+ return new Date();
+ }
+ }
+ return new Date();
+ }
+
+ /**
+ * @param document
+ * @return Returns the url attribute from the imageInfo node
+ */
+ private static String getImageUrl(Node document) {
+ Element element = (Element) getImageInfo(document);
+ if (element != null) {
+ return element.getAttribute("url");
+ }
+ return null;
+ }
+
+ /**
+ * Takes the node document and gives out the attribute length from the node document
+ * @param document
+ * @return
+ */
+ private static long getDataLength(Node document) {
+ Element element = (Element) document;
+ if (element != null) {
+ String length = element.getAttribute("length");
+ if (length != null && !length.equals("")) {
+ return Long.parseLong(length);
+ }
+ }
+ return 0L;
+ }
+
+ /**
+ * Generic method to get the value of any meta as returned by the getMetaData function
+ * @param document node document as returned by API
+ * @param metaName the name of meta node to be returned
+ * @return
+ */
+ private static String getMetaDataValue(Node document, String metaName) {
+ Element metaData = getMetaData(document, metaName);
+ if (metaData != null) {
+ return metaData.getAttribute("value");
+ }
+ return null;
+ }
+
+ /**
+ * Generic method to return an element taking the node document and metaName as input
+ * @param document node document as returned by API
+ * @param metaName the name of meta node to be returned
+ * @return
+ */
+ @Nullable
+ private static Element getMetaData(Node document, String metaName) {
+ Node extraMetaData = getExtraMetaData(document);
+ if (extraMetaData != null) {
+ Node node = getNode(extraMetaData, metaName);
+ if (node != null) {
+ return (Element) node;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Extracts extmetadata from the response XML
+ * @param document
+ * @return
+ */
+ @Nullable
+ private static Node getExtraMetaData(Node document) {
+ Node imageInfo = getImageInfo(document);
+ if (imageInfo != null) {
+ return getNode(imageInfo, "extmetadata");
+ }
+ return null;
+ }
+
+ /**
+ * Extracts the ii node from the imageinfo node
+ * @param document
+ * @return
+ */
+ @Nullable
+ private static Node getImageInfo(Node document) {
+ Node imageInfo = getNode(document, "imageinfo");
+ if (imageInfo != null) {
+ return getNode(imageInfo, "ii");
+ }
+ return null;
+ }
+
+ /**
+ * Takes a parent node as input and returns a child node if present
+ * @param node parent node
+ * @param nodeName child node name
+ * @return
+ */
+ @Nullable
+ public static Node getNode(Node node, String nodeName) {
+ NodeList childNodes = node.getChildNodes();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Node nodeItem = childNodes.item(i);
+ Element item = (Element) nodeItem;
+ if (item.getTagName().equals(nodeName)) {
+ return nodeItem;
+ }
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java
new file mode 100644
index 000000000..1f385b258
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java
@@ -0,0 +1,160 @@
+package fr.free.nrw.commons.category;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.DataSetObserver;
+import android.os.Bundle;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.view.View;
+import android.widget.AdapterView;
+
+import butterknife.ButterKnife;
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.auth.AuthenticatedActivity;
+import fr.free.nrw.commons.media.MediaDetailPagerFragment;
+import timber.log.Timber;
+
+/**
+ * This activity displays pictures of a particular category
+ * Its generic and simply takes the name of category name in its start intent to load all images in
+ * a particular category. This activity is currently being used to display a list of featured images,
+ * which is nothing but another category on wikimedia commons.
+ */
+
+public class CategoryImagesActivity
+ extends AuthenticatedActivity
+ implements FragmentManager.OnBackStackChangedListener,
+ MediaDetailPagerFragment.MediaDetailProvider,
+ AdapterView.OnItemClickListener{
+
+
+ private FragmentManager supportFragmentManager;
+ private CategoryImagesListFragment categoryImagesListFragment;
+ private MediaDetailPagerFragment mediaDetails;
+
+ @Override
+ protected void onAuthCookieAcquired(String authCookie) {
+
+ }
+
+ @Override
+ protected void onAuthFailure() {
+
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_category_images);
+ ButterKnife.bind(this);
+
+ // Activity can call methods in the fragment by acquiring a
+ // reference to the Fragment from FragmentManager, using findFragmentById()
+ supportFragmentManager = getSupportFragmentManager();
+ setCategoryImagesFragment();
+ supportFragmentManager.addOnBackStackChangedListener(this);
+ if (savedInstanceState != null) {
+ mediaDetails = (MediaDetailPagerFragment) supportFragmentManager
+ .findFragmentById(R.id.fragmentContainer);
+
+ }
+ requestAuthToken();
+ initDrawer();
+ setPageTitle();
+ }
+
+ /**
+ * Gets the categoryName from the intent and initializes the fragment for showing images of that category
+ */
+ private void setCategoryImagesFragment() {
+ categoryImagesListFragment = new CategoryImagesListFragment();
+ String categoryName = getIntent().getStringExtra("categoryName");
+ if (getIntent() != null && categoryName != null) {
+ Bundle arguments = new Bundle();
+ arguments.putString("categoryName", categoryName);
+ categoryImagesListFragment.setArguments(arguments);
+ FragmentTransaction transaction = supportFragmentManager.beginTransaction();
+ transaction
+ .add(R.id.fragmentContainer, categoryImagesListFragment)
+ .commit();
+ }
+ }
+
+ /**
+ * Gets the passed title from the intents and displays it as the page title
+ */
+ private void setPageTitle() {
+ if (getIntent() != null && getIntent().getStringExtra("title") != null) {
+ setTitle(getIntent().getStringExtra("title"));
+ }
+ }
+
+ @Override
+ public void onBackStackChanged() {
+ }
+
+ @Override
+ public void onItemClick(AdapterView> adapterView, View view, int i, long l) {
+ if (mediaDetails == null || !mediaDetails.isVisible()) {
+ // set isFeaturedImage true for featured images, to include author field on media detail
+ mediaDetails = new MediaDetailPagerFragment(false, true);
+ FragmentManager supportFragmentManager = getSupportFragmentManager();
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.fragmentContainer, mediaDetails)
+ .addToBackStack(null)
+ .commit();
+ supportFragmentManager.executePendingTransactions();
+ }
+ mediaDetails.showImage(i);
+ }
+
+ /**
+ * Consumers should be simply using this method to use this activity.
+ * @param context
+ * @param title Page title
+ * @param categoryName Name of the category for displaying its images
+ */
+ public static void startYourself(Context context, String title, String categoryName) {
+ Intent intent = new Intent(context, CategoryImagesActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ intent.putExtra("title", title);
+ intent.putExtra("categoryName", categoryName);
+ context.startActivity(intent);
+ }
+
+ @Override
+ public Media getMediaAtPosition(int i) {
+ if (categoryImagesListFragment.getAdapter() == null) {
+ // not yet ready to return data
+ return null;
+ } else {
+ return (Media) categoryImagesListFragment.getAdapter().getItem(i);
+ }
+ }
+
+ @Override
+ public int getTotalMediaCount() {
+ if (categoryImagesListFragment.getAdapter() == null) {
+ return 0;
+ }
+ return categoryImagesListFragment.getAdapter().getCount();
+ }
+
+ @Override
+ public void notifyDatasetChanged() {
+
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java
new file mode 100644
index 000000000..3b6734edd
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java
@@ -0,0 +1,227 @@
+package fr.free.nrw.commons.category;
+
+import android.annotation.SuppressLint;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.GridView;
+import android.widget.ListAdapter;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import dagger.android.support.DaggerFragment;
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.utils.NetworkUtils;
+import fr.free.nrw.commons.utils.ViewUtil;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.schedulers.Schedulers;
+import timber.log.Timber;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+/**
+ * Displays images for a particular category with load more on scrolling incorporated
+ */
+public class CategoryImagesListFragment extends DaggerFragment {
+
+ private static int TIMEOUT_SECONDS = 15;
+
+ private GridViewAdapter gridAdapter;
+
+ @BindView(R.id.statusMessage)
+ TextView statusTextView;
+ @BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar;
+ @BindView(R.id.categoryImagesList) GridView gridView;
+
+ private boolean hasMoreImages = true;
+ private boolean isLoading;
+ private String categoryName = null;
+
+ @Inject CategoryImageController controller;
+ @Inject @Named("category_prefs") SharedPreferences categoryPreferences;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.fragment_category_images, container, false);
+ ButterKnife.bind(this, v);
+ return v;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
+ initViews();
+ }
+
+ /**
+ * Initializes the UI elements for the fragment
+ * Setup the grid view to and scroll listener for it
+ */
+ private void initViews() {
+ String categoryName = getArguments().getString("categoryName");
+ if (getArguments() != null && categoryName != null) {
+ this.categoryName = categoryName;
+ resetQueryContinueValues(categoryName);
+ initList();
+ setScrollListener();
+ }
+ }
+
+ /**
+ * Query continue values determine the last page that was loaded for the particular keyword
+ * This method resets those values, so that the results can be queried from the first page itself
+ * @param keyword
+ */
+ private void resetQueryContinueValues(String keyword) {
+ SharedPreferences.Editor editor = categoryPreferences.edit();
+ editor.remove(keyword);
+ editor.apply();
+ }
+
+ /**
+ * Checks for internet connection and then initializes the grid view with first 10 images of that category
+ */
+ @SuppressLint("CheckResult")
+ private void initList() {
+ if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
+ handleNoInternet();
+ return;
+ }
+
+ isLoading = true;
+ progressBar.setVisibility(VISIBLE);
+ Observable.fromCallable(() -> controller.getCategoryImages(categoryName))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .subscribe(this::handleSuccess, this::handleError);
+ }
+
+ /**
+ * Handles the UI updates for no internet scenario
+ */
+ private void handleNoInternet() {
+ progressBar.setVisibility(GONE);
+ if (gridAdapter == null || gridAdapter.isEmpty()) {
+ statusTextView.setVisibility(VISIBLE);
+ statusTextView.setText(getString(R.string.no_internet));
+ } else {
+ ViewUtil.showSnackbar(gridView, R.string.no_internet);
+ }
+ }
+
+ /**
+ * Logs and handles API error scenario
+ * @param throwable
+ */
+ private void handleError(Throwable throwable) {
+ Timber.e(throwable, "Error occurred while loading featured images");
+ initErrorView();
+ }
+
+ /**
+ * Handles the UI updates for a error scenario
+ */
+ private void initErrorView() {
+ ViewUtil.showSnackbar(gridView, R.string.error_loading_images);
+ progressBar.setVisibility(GONE);
+ if (gridAdapter == null || gridAdapter.isEmpty()) {
+ statusTextView.setVisibility(VISIBLE);
+ statusTextView.setText(getString(R.string.no_images_found));
+ } else {
+ statusTextView.setVisibility(GONE);
+ }
+ }
+
+ /**
+ * Initializes the adapter with a list of Media objects
+ * @param mediaList
+ */
+ private void setAdapter(List mediaList) {
+ gridAdapter = new GridViewAdapter(this.getContext(), R.layout.layout_category_images, mediaList);
+ gridView.setAdapter(gridAdapter);
+ }
+
+ /**
+ * Sets the scroll listener for the grid view so that more images are fetched when the user scrolls down
+ * Checks if the category has more images before loading
+ * Also checks whether images are currently being fetched before triggering another request
+ */
+ private void setScrollListener() {
+ gridView.setOnScrollListener(new AbsListView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ if (hasMoreImages && !isLoading && (firstVisibleItem + visibleItemCount + 1 >= totalItemCount)) {
+ isLoading = true;
+ fetchMoreImages();
+ }
+ }
+ });
+ }
+
+ /**
+ * Fetches more images for the category and adds it to the grid view adapter
+ */
+ @SuppressLint("CheckResult")
+ private void fetchMoreImages() {
+ if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
+ handleNoInternet();
+ return;
+ }
+
+ progressBar.setVisibility(VISIBLE);
+ Observable.fromCallable(() -> controller.getCategoryImages(categoryName))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .subscribe(this::handleSuccess, this::handleError);
+ }
+
+ /**
+ * Handles the success scenario
+ * On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
+ * @param collection
+ */
+ private void handleSuccess(List collection) {
+ if(collection == null || collection.isEmpty()) {
+ initErrorView();
+ hasMoreImages = false;
+ return;
+ }
+
+ if(gridAdapter == null) {
+ setAdapter(collection);
+ } else {
+ gridAdapter.addItems(collection);
+ }
+
+ progressBar.setVisibility(GONE);
+ isLoading = false;
+ statusTextView.setVisibility(GONE);
+ }
+
+ public ListAdapter getAdapter() {
+ return gridView.getAdapter();
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java
new file mode 100644
index 000000000..c8e6066f6
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java
@@ -0,0 +1,88 @@
+package fr.free.nrw.commons.category;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.MediaWikiImageView;
+import fr.free.nrw.commons.R;
+
+/**
+ * This is created to only display UI implementation. Needs to be changed in real implementation
+ */
+
+public class GridViewAdapter extends ArrayAdapter {
+ private Context context;
+ private List data;
+
+ public GridViewAdapter(Context context, int layoutResourceId, List data) {
+ super(context, layoutResourceId, data);
+ this.context = context;
+ this.data = data;
+ }
+
+ /**
+ * Adds more item to the list
+ * Its triggered on scrolling down in the list
+ * @param images
+ */
+ public void addItems(List images) {
+ if (data == null) {
+ data = new ArrayList<>();
+ }
+ data.addAll(images);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return data == null || data.isEmpty();
+ }
+
+ /**
+ * Sets up the UI for the category image item
+ * @param position
+ * @param convertView
+ * @param parent
+ * @return
+ */
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+
+ if (convertView == null) {
+ LayoutInflater inflater = ((Activity) context).getLayoutInflater();
+ convertView = inflater.inflate(R.layout.layout_category_images, null);
+ }
+
+ Media item = data.get(position);
+ MediaWikiImageView imageView = convertView.findViewById(R.id.categoryImageView);
+ TextView fileName = convertView.findViewById(R.id.categoryImageTitle);
+ TextView author = convertView.findViewById(R.id.categoryImageAuthor);
+ fileName.setText(item.getFilename());
+ setAuthorView(item, author);
+ imageView.setMedia(item);
+ return convertView;
+ }
+
+ /**
+ * Shows author information if its present
+ * @param item
+ * @param author
+ */
+ private void setAuthorView(Media item, TextView author) {
+ if (item.getCreator() != null && !item.getCreator().equals("")) {
+ String uploadedByTemplate = context.getString(R.string.image_uploaded_by);
+ author.setText(String.format(uploadedByTemplate, item.getCreator()));
+ } else {
+ author.setVisibility(View.GONE);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/category/QueryContinue.java b/app/src/main/java/fr/free/nrw/commons/category/QueryContinue.java
new file mode 100644
index 000000000..e12d5a778
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/QueryContinue.java
@@ -0,0 +1,24 @@
+package fr.free.nrw.commons.category;
+
+/**
+ * For APIs that return paginated responses, MediaWiki APIs uses the QueryContinue to facilitate fetching of subsequent pages
+ * https://www.mediawiki.org/wiki/API:Raw_query_continue
+ */
+public class QueryContinue {
+ private String continueParam;
+ private String gcmContinueParam;
+
+ public QueryContinue(String continueParam, String gcmContinueParam) {
+ this.continueParam = continueParam;
+ this.gcmContinueParam = gcmContinueParam;
+ }
+
+ public String getGcmContinueParam() {
+ return gcmContinueParam;
+ }
+
+ public String getContinueParam() {
+ return continueParam;
+ }
+}
+
diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
index f88f3b34a..51aa85903 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
@@ -7,7 +7,7 @@ import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
-import fr.free.nrw.commons.featured.FeaturedImagesActivity;
+import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
@@ -49,5 +49,5 @@ public abstract class ActivityBuilderModule {
abstract NotificationActivity bindNotificationActivity();
@ContributesAndroidInjector
- abstract FeaturedImagesActivity bindFeaturedImagesActivity();
+ abstract CategoryImagesActivity bindFeaturedImagesActivity();
}
diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
index e8b915c7e..55281be7e 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
@@ -6,6 +6,8 @@ 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;
@@ -85,6 +87,17 @@ public class CommonsApplicationModule {
return context.getSharedPreferences("prefs", MODE_PRIVATE);
}
+ /**
+ *
+ * @param context
+ * @return returns categoryPrefs
+ */
+ @Provides
+ @Named("category_prefs")
+ public SharedPreferences providesCategorySharedPreferences(Context context) {
+ return context.getSharedPreferences("categoryPrefs", MODE_PRIVATE);
+ }
+
@Provides
@Named("direct_nearby_upload_prefs")
public SharedPreferences providesDirectNearbyUploadPreferences(Context context) {
@@ -106,8 +119,11 @@ public class CommonsApplicationModule {
@Provides
@Singleton
- public MediaWikiApi provideMediaWikiApi(Context context, @Named("default_preferences") SharedPreferences sharedPreferences) {
- return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, sharedPreferences);
+ 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
@@ -116,6 +132,16 @@ public class CommonsApplicationModule {
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() {
diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
index c5cdcb5a7..dfed64871 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
@@ -4,7 +4,7 @@ import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.contributions.ContributionsListFragment;
-import fr.free.nrw.commons.featured.FeaturedImagesListFragment;
+import fr.free.nrw.commons.category.CategoryImagesListFragment;
import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.nearby.NearbyListFragment;
@@ -49,6 +49,6 @@ public abstract class FragmentBuilderModule {
abstract SingleUploadFragment bindSingleUploadFragment();
@ContributesAndroidInjector
- abstract FeaturedImagesListFragment bindFeaturedImagesListFragment();
+ abstract CategoryImagesListFragment bindFeaturedImagesListFragment();
}
diff --git a/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImage.java b/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImage.java
deleted file mode 100644
index 853fba29e..000000000
--- a/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImage.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package fr.free.nrw.commons.featured;
-
-
-import fr.free.nrw.commons.Media;
-
-/**
- * Object to hold FeaturedImage
- */
-
-public class FeaturedImage {
- private Media image;
- private String author;
- private String fileName;
-
- public FeaturedImage(Media image, String author, String fileName) {
- this.image = image;
- this.author = author;
- this.fileName = fileName;
- }
-
- public Media getImage() {
- return image;
- }
-
- public void setImage(Media image) {
- this.image = image;
- }
-
- public String getAuthor() {
- return author;
- }
-
- public void setAuthor(String author) {
- this.author = author;
- }
-
- public String getFileName() {
- return fileName;
- }
-
- public void setFileName(String fileName) {
- this.fileName = fileName;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImagesActivity.java b/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImagesActivity.java
deleted file mode 100644
index a2dc7b00b..000000000
--- a/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImagesActivity.java
+++ /dev/null
@@ -1,114 +0,0 @@
-package fr.free.nrw.commons.featured;
-
-import android.database.DataSetObserver;
-import android.os.Bundle;
-import android.support.v4.app.FragmentManager;
-import android.view.View;
-import android.widget.AdapterView;
-
-import butterknife.ButterKnife;
-import fr.free.nrw.commons.Media;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.auth.AuthenticatedActivity;
-import fr.free.nrw.commons.media.MediaDetailPagerFragment;
-
-/**
- * This activity displays pic of the days of last xx days
- */
-
-public class FeaturedImagesActivity
- extends AuthenticatedActivity
- implements FragmentManager.OnBackStackChangedListener,
- MediaDetailPagerFragment.MediaDetailProvider,
- AdapterView.OnItemClickListener{
-
- private FeaturedImagesListFragment featuredImagesListFragment;
- private MediaDetailPagerFragment mediaDetails;
-
- @Override
- protected void onAuthCookieAcquired(String authCookie) {
-
- }
-
- @Override
- protected void onAuthFailure() {
-
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_featured_images);
- ButterKnife.bind(this);
-
- // Activity can call methods in the fragment by acquiring a
- // reference to the Fragment from FragmentManager, using findFragmentById()
- FragmentManager supportFragmentManager = getSupportFragmentManager();
- featuredImagesListFragment = (FeaturedImagesListFragment)supportFragmentManager
- .findFragmentById(R.id.featuedListFragment);
-
- supportFragmentManager.addOnBackStackChangedListener(this);
- if (savedInstanceState != null) {
- mediaDetails = (MediaDetailPagerFragment)supportFragmentManager
- .findFragmentById(R.id.featuredFragmentContainer);
-
- }
- requestAuthToken();
- initDrawer();
- setTitle(getString(R.string.title_activity_featured_images));
- }
-
- @Override
- public void onBackStackChanged() {
-
- }
-
- @Override
- public void onItemClick(AdapterView> adapterView, View view, int i, long l) {
- if (mediaDetails == null || !mediaDetails.isVisible()) {
- // set isFeaturedImage true for featured images, to include author field on media detail
- mediaDetails = new MediaDetailPagerFragment(false, true);
- FragmentManager supportFragmentManager = getSupportFragmentManager();
- supportFragmentManager
- .beginTransaction()
- .replace(R.id.featuredFragmentContainer, mediaDetails)
- .addToBackStack(null)
- .commit();
- supportFragmentManager.executePendingTransactions();
- }
- mediaDetails.showImage(i);
- }
-
- @Override
- public Media getMediaAtPosition(int i) {
- if (featuredImagesListFragment.getAdapter() == null) {
- // not yet ready to return data
- return null;
- } else {
- return ((FeaturedImage)featuredImagesListFragment.getAdapter().getItem(i)).getImage();
- }
- }
-
- @Override
- public int getTotalMediaCount() {
- if (featuredImagesListFragment.getAdapter() == null) {
- return 0;
- }
- return featuredImagesListFragment.getAdapter().getCount();
- }
-
- @Override
- public void notifyDatasetChanged() {
-
- }
-
- @Override
- public void registerDataSetObserver(DataSetObserver observer) {
-
- }
-
- @Override
- public void unregisterDataSetObserver(DataSetObserver observer) {
-
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImagesListFragment.java b/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImagesListFragment.java
deleted file mode 100644
index 19e33b0ee..000000000
--- a/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImagesListFragment.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package fr.free.nrw.commons.featured;
-
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.GridView;
-import android.widget.ListAdapter;
-
-import java.util.ArrayList;
-
-import butterknife.ButterKnife;
-import dagger.android.support.DaggerFragment;
-import fr.free.nrw.commons.Media;
-import fr.free.nrw.commons.R;
-
-public class FeaturedImagesListFragment extends DaggerFragment {
- private GridView gridView;
- private MockGridViewAdapter gridAdapter;
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View v = inflater.inflate(R.layout.fragment_featured_images, container, false);
- ButterKnife.bind(this, v);
- return v;
- }
-
- @Override
- public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- gridView = getView().findViewById(R.id.featuredImagesList);
- gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
- gridAdapter = new MockGridViewAdapter(this.getContext(), R.layout.layout_featured_images, getMockFeaturedImages());
- gridView.setAdapter(gridAdapter);
-
- }
-
- private ArrayList getMockFeaturedImages(){
- ArrayList featuredImages = new ArrayList<>();
- for (int i=0; i<10; i++){
- featuredImages.add(new FeaturedImage(new Media("test.jpg"), "username: test", "test file name"));
- }
- return featuredImages;
- }
-
- public ListAdapter getAdapter() {
- return gridView.getAdapter();
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/featured/MockGridViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/featured/MockGridViewAdapter.java
deleted file mode 100644
index 7aa2a8892..000000000
--- a/app/src/main/java/fr/free/nrw/commons/featured/MockGridViewAdapter.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package fr.free.nrw.commons.featured;
-
-import android.app.Activity;
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-
-import fr.free.nrw.commons.MediaWikiImageView;
-import fr.free.nrw.commons.R;
-
-/**
- * This is created to only display UI implementation. Needs to be changed in real implementation
- */
-
-public class MockGridViewAdapter extends ArrayAdapter {
- private Context context;
- private int layoutResourceId;
- private ArrayList data = new ArrayList();
-
- public MockGridViewAdapter(Context context, int layoutResourceId, ArrayList data) {
- super(context, layoutResourceId, data);
- this.layoutResourceId = layoutResourceId;
- this.context = context;
- this.data = data;
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
-
- if (convertView == null) {
- LayoutInflater inflater = ((Activity) context).getLayoutInflater();
- convertView = inflater.inflate(R.layout.layout_featured_images, null);
- }
-
- FeaturedImage item = data.get(position);
- MediaWikiImageView imageView = convertView.findViewById(R.id.featuredImageView);
- TextView fileName = convertView.findViewById(R.id.featuredImageTitle);
- TextView author = convertView.findViewById(R.id.featuredImageAuthor);
- fileName.setText("Test file name");
- author.setText("Uploaded by: Test user name");
- imageView.setMedia(item.getImage());
- return convertView;
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java
index 037f92e56..c3a977ab5 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java
@@ -45,6 +45,7 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.ui.widget.CompatTextView;
import timber.log.Timber;
+import static android.view.View.*;
import static android.widget.Toast.LENGTH_SHORT;
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@@ -154,9 +155,9 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
authorLayout = (LinearLayout) view.findViewById(R.id.authorLinearLayout);
if (isFeaturedMedia){
- authorLayout.setVisibility(View.VISIBLE);
+ authorLayout.setVisibility(VISIBLE);
} else {
- authorLayout.setVisibility(View.GONE);
+ authorLayout.setVisibility(GONE);
}
licenseList = new LicenseList(getActivity());
@@ -306,6 +307,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
rebuildCatList();
+ if(media.getCreator() == null || media.getCreator().equals("")) {
+ authorLayout.setVisibility(GONE);
+ } else {
+ author.setText(media.getCreator());
+ }
+
checkDeletion(media);
}
@@ -313,13 +320,17 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if (licenseLink(media) != null) {
license.setOnClickListener(v -> openWebBrowser(licenseLink(media)));
} else {
- Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT);
- toast.show();
+ if(isFeaturedMedia) {
+ 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);
+ toast.show();
+ }
}
if (media.getCoordinates() != null) {
coordinates.setOnClickListener(v -> openMap(media.getCoordinates()));
}
- if (delete.getVisibility() == View.VISIBLE) {
+ if (delete.getVisibility() == VISIBLE) {
enableDeleteButton(true);
delete.setOnClickListener(v -> {
@@ -369,7 +380,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
});
}
- if (nominatedforDeletion.getVisibility() == View.VISIBLE){
+ if (nominatedforDeletion.getVisibility() == VISIBLE){
seeMore.setOnClickListener(v -> {
openWebBrowser(media.getFilePageTitle().getMobileUri().toString());
});
@@ -476,12 +487,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private void checkDeletion(Media media){
if (media.getRequestedDeletion()){
- delete.setVisibility(View.GONE);
- nominatedforDeletion.setVisibility(View.VISIBLE);
+ delete.setVisibility(GONE);
+ nominatedforDeletion.setVisibility(VISIBLE);
}
else{
- delete.setVisibility(View.VISIBLE);
- nominatedforDeletion.setVisibility(View.GONE);
+ delete.setVisibility(VISIBLE);
+ nominatedforDeletion.setVisibility(GONE);
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java
index 78051abd8..6629d0933 100644
--- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java
+++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java
@@ -9,6 +9,8 @@ import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
+import com.google.gson.Gson;
+
import org.apache.http.HttpResponse;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
@@ -38,7 +40,10 @@ import java.util.Locale;
import java.util.concurrent.Callable;
import fr.free.nrw.commons.BuildConfig;
+import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.PageTitle;
+import fr.free.nrw.commons.category.CategoryImageUtils;
+import fr.free.nrw.commons.category.QueryContinue;
import fr.free.nrw.commons.notification.Notification;
import fr.free.nrw.commons.notification.NotificationUtils;
import in.yuvi.http.fluent.Http;
@@ -46,6 +51,8 @@ import io.reactivex.Observable;
import io.reactivex.Single;
import timber.log.Timber;
+import static fr.free.nrw.commons.utils.ContinueUtils.getQueryContinue;
+
/**
* @author Addshore
*/
@@ -56,9 +63,15 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private AbstractHttpClient httpClient;
private MWApi api;
private Context context;
- private SharedPreferences sharedPreferences;
+ private SharedPreferences defaultPreferences;
+ private SharedPreferences categoryPreferences;
+ private Gson gson;
- public ApacheHttpClientMediaWikiApi(Context context, String apiURL, SharedPreferences sharedPreferences) {
+ public ApacheHttpClientMediaWikiApi(Context context,
+ String apiURL,
+ SharedPreferences defaultPreferences,
+ SharedPreferences categoryPreferences,
+ Gson gson) {
this.context = context;
BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
@@ -69,7 +82,9 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent());
httpClient = new DefaultHttpClient(cm, params);
api = new MWApi(apiURL, httpClient);
- this.sharedPreferences = sharedPreferences;
+ this.defaultPreferences = defaultPreferences;
+ this.categoryPreferences = categoryPreferences;
+ this.gson = gson;
}
@Override
@@ -160,7 +175,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
}
private void setAuthCookieOnLogin(boolean isLoggedIn) {
- SharedPreferences.Editor editor = sharedPreferences.edit();
+ SharedPreferences.Editor editor = defaultPreferences.edit();
if (isLoggedIn) {
editor.putBoolean("isUserLoggedIn", true);
editor.putString("getAuthCookie", api.getAuthCookie());
@@ -448,6 +463,81 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return NotificationUtils.getNotificationsFromList(context, 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 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("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<>();
+ }
+
+ QueryContinue queryContinue = getQueryContinue(apiResult.getNode("/api/continue").getDocument());
+ setQueryContinueValues(categoryName, queryContinue);
+
+ NodeList childNodes = categoryImagesNode.getDocument().getChildNodes();
+ return CategoryImageUtils.getMediaList(childNodes);
+ }
+
+ /**
+ * 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
+ * After fetching images a page of image for a particular category, shared prefs are updated with the latest QueryContinue Values
+ * @param keyword
+ * @param queryContinue
+ */
+ private void setQueryContinueValues(String keyword, QueryContinue queryContinue) {
+ SharedPreferences.Editor editor = categoryPreferences.edit();
+ editor.putString(keyword, gson.toJson(queryContinue));
+ editor.apply();
+ }
+
+ /**
+ * Before making a paginated API call, this method is called to get the latest query continue values to be used
+ * @param keyword
+ * @return
+ */
+ @Nullable
+ private QueryContinue getQueryContinueValues(String keyword) {
+ String queryContinueString = categoryPreferences.getString(keyword, null);
+ return gson.fromJson(queryContinueString, QueryContinue.class);
+ }
+
@Override
public boolean existingFile(String fileSha1) throws IOException {
return api.action("query")
diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java
index fd213455d..c0bd2fd87 100644
--- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java
+++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java
@@ -7,6 +7,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.List;
+import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.notification.Notification;
import io.reactivex.Observable;
import io.reactivex.Single;
@@ -34,6 +35,8 @@ public interface MediaWikiApi {
boolean logEvents(LogBuilder[] logBuilders);
+ List getCategoryImages(String categoryName);
+
@NonNull
UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener) throws IOException;
diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java
index acd9b7646..4a7322b57 100644
--- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java
@@ -23,12 +23,11 @@ import fr.free.nrw.commons.AboutActivity;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
-import fr.free.nrw.commons.featured.FeaturedImagesActivity;
+import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
@@ -37,6 +36,8 @@ import timber.log.Timber;
public abstract class NavigationBaseActivity extends BaseActivity
implements NavigationView.OnNavigationItemSelectedListener {
+ private static final String FEATURED_IMAGES_CATEGORY = "Category:Featured_pictures_on_Wikimedia_Commons";
+
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.navigation_view)
@@ -157,7 +158,7 @@ public abstract class NavigationBaseActivity extends BaseActivity
return true;
case R.id.action_featured_images:
drawerLayout.closeDrawer(navigationView);
- startActivityWithFlags(this, FeaturedImagesActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_featured_images), FEATURED_IMAGES_CATEGORY);
return true;
default:
Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ContinueUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ContinueUtils.java
new file mode 100644
index 000000000..b05c8bc45
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/utils/ContinueUtils.java
@@ -0,0 +1,15 @@
+package fr.free.nrw.commons.utils;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import fr.free.nrw.commons.category.QueryContinue;
+
+public class ContinueUtils {
+
+ public static QueryContinue getQueryContinue(Node document) {
+ Element continueElement = (Element) document;
+ return new QueryContinue(continueElement.getAttribute("continue"),
+ continueElement.getAttribute("gcmcontinue"));
+ }
+}
diff --git a/app/src/main/res/layout/activity_featured_images.xml b/app/src/main/res/layout/activity_category_images.xml
similarity index 69%
rename from app/src/main/res/layout/activity_featured_images.xml
rename to app/src/main/res/layout/activity_category_images.xml
index 755fe4983..c329e4458 100644
--- a/app/src/main/res/layout/activity_featured_images.xml
+++ b/app/src/main/res/layout/activity_category_images.xml
@@ -1,6 +1,5 @@
-
-
diff --git a/app/src/main/res/layout/fragment_featured_images.xml b/app/src/main/res/layout/fragment_category_images.xml
similarity index 70%
rename from app/src/main/res/layout/fragment_featured_images.xml
rename to app/src/main/res/layout/fragment_category_images.xml
index ca45f44c3..001f0a780 100644
--- a/app/src/main/res/layout/fragment_featured_images.xml
+++ b/app/src/main/res/layout/fragment_category_images.xml
@@ -1,20 +1,21 @@
-
+ android:background="?attr/mainBackground">
@@ -33,7 +33,7 @@
android:padding="@dimen/small_gap"
>
Proceed
Cancel
Retry
+
+ No images found!
+ Error occurred while loading images.
+ Uploaded by: %1$s
Share App
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt
index 73760dd40..b1de29143 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt
@@ -3,6 +3,7 @@ package fr.free.nrw.commons
import android.content.Context
import android.content.SharedPreferences
import android.support.v4.util.LruCache
+import com.google.gson.Gson
import com.nhaarman.mockito_kotlin.mock
import com.squareup.leakcanary.RefWatcher
import fr.free.nrw.commons.auth.AccountUtil
@@ -36,6 +37,7 @@ class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModu
val accountUtil: AccountUtil = mock()
val appSharedPreferences: SharedPreferences = mock()
val defaultSharedPreferences: SharedPreferences = mock()
+ val categorySharedPreferences: SharedPreferences = mock()
val otherSharedPreferences: SharedPreferences = mock()
val uploadController: UploadController = mock()
val mockSessionManager: SessionManager = mock()
@@ -45,6 +47,7 @@ class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModu
val mockDbOpenHelper: DBOpenHelper = mock()
val nearbyPlaces: NearbyPlaces = mock()
val lruCache: LruCache = mock()
+ val gson: Gson = Gson()
override fun providesAccountUtil(context: Context): AccountUtil = accountUtil
@@ -58,7 +61,7 @@ class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModu
override fun providesSessionManager(context: Context, mediaWikiApi: MediaWikiApi, sharedPreferences: SharedPreferences): SessionManager = mockSessionManager
- override fun provideMediaWikiApi(context: Context, sharedPreferences: SharedPreferences): MediaWikiApi = mediaWikiApi
+ override fun provideMediaWikiApi(context: Context, sharedPreferences: SharedPreferences, categorySharedPreferences: SharedPreferences, gson: Gson): MediaWikiApi = mediaWikiApi
override fun provideLocationServiceManager(context: Context): LocationServiceManager = locationServiceManager
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt
index c51d354c2..686a90ef2 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt
@@ -3,6 +3,7 @@ package fr.free.nrw.commons.mwapi
import android.content.SharedPreferences
import android.os.Build
import android.preference.PreferenceManager
+import com.google.gson.Gson
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.TestCommonsApplication
import okhttp3.mockwebserver.MockResponse
@@ -26,12 +27,14 @@ class ApacheHttpClientMediaWikiApiTest {
private lateinit var testObject: ApacheHttpClientMediaWikiApi
private lateinit var server: MockWebServer
private lateinit var sharedPreferences: SharedPreferences
+ private lateinit var categoryPreferences: SharedPreferences
@Before
fun setUp() {
server = MockWebServer()
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application)
- testObject = ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.hostName + ":" + server.port + "/", sharedPreferences)
+ categoryPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application)
+ testObject = ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.hostName + ":" + server.port + "/", sharedPreferences, categoryPreferences, Gson())
testObject.setWikiMediaToolforgeUrl("http://" + server.hostName + ":" + server.port + "/")
}