From 9845a6265d7a1339998413fe1770931264a1af34 Mon Sep 17 00:00:00 2001 From: Vivek Maskara Date: Mon, 7 May 2018 13:39:23 +0530 Subject: [PATCH] Integrate API for displaying featured images (#1456) * Integrate API for displaying featured images * Add pagination and refactor code so that it can be reused for category images * Add license info to the images * Fix author view * Remove unused values * Fix minor issues with featured images * Fix null license url issue * Remove some log lines * Fix back navigation issue * fix tests * fix test inits * Gracefully handling various error situations * Added java docs --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 5 +- .../category/CategoryImageController.java | 29 +++ .../commons/category/CategoryImageUtils.java | 225 +++++++++++++++++ .../category/CategoryImagesActivity.java | 160 ++++++++++++ .../category/CategoryImagesListFragment.java | 227 ++++++++++++++++++ .../nrw/commons/category/GridViewAdapter.java | 88 +++++++ .../nrw/commons/category/QueryContinue.java | 24 ++ .../nrw/commons/di/ActivityBuilderModule.java | 4 +- .../commons/di/CommonsApplicationModule.java | 30 ++- .../nrw/commons/di/FragmentBuilderModule.java | 4 +- .../nrw/commons/featured/FeaturedImage.java | 44 ---- .../featured/FeaturedImagesActivity.java | 114 --------- .../featured/FeaturedImagesListFragment.java | 52 ---- .../commons/featured/MockGridViewAdapter.java | 50 ---- .../commons/media/MediaDetailFragment.java | 31 ++- .../mwapi/ApacheHttpClientMediaWikiApi.java | 98 +++++++- .../free/nrw/commons/mwapi/MediaWikiApi.java | 3 + .../commons/theme/NavigationBaseActivity.java | 7 +- .../free/nrw/commons/utils/ContinueUtils.java | 15 ++ ...mages.xml => activity_category_images.xml} | 11 +- ...mages.xml => fragment_category_images.xml} | 15 +- ..._images.xml => layout_category_images.xml} | 10 +- app/src/main/res/values/strings.xml | 4 + .../nrw/commons/TestCommonsApplication.kt | 5 +- .../mwapi/ApacheHttpClientMediaWikiApiTest.kt | 5 +- 26 files changed, 953 insertions(+), 309 deletions(-) create mode 100644 app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/category/QueryContinue.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/featured/FeaturedImage.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/featured/FeaturedImagesActivity.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/featured/FeaturedImagesListFragment.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/featured/MockGridViewAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ContinueUtils.java rename app/src/main/res/layout/{activity_featured_images.xml => activity_category_images.xml} (69%) rename app/src/main/res/layout/{fragment_featured_images.xml => fragment_category_images.xml} (70%) rename app/src/main/res/layout/{layout_featured_images.xml => layout_category_images.xml} (88%) 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 + "/") }