mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
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
This commit is contained in:
parent
96eae8c0a6
commit
9845a6265d
26 changed files with 953 additions and 309 deletions
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -92,8 +92,9 @@
|
|||
android:label="@string/navigation_item_notification" />
|
||||
|
||||
<activity
|
||||
android:name=".featured.FeaturedImagesActivity"
|
||||
android:label="@string/title_activity_featured_images" />
|
||||
android:name=".category.CategoryImagesActivity"
|
||||
android:label="@string/title_activity_featured_images"
|
||||
android:parentActivityName=".contributions.ContributionsActivity" />
|
||||
|
||||
<service android:name=".upload.UploadService" />
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Media> getCategoryImages(String categoryName) {
|
||||
return mediaWikiApi.getCategoryImages(categoryName);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Media> getMediaList(NodeList childNodes) {
|
||||
List<Media> 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Media> 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<Media> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Media> data;
|
||||
|
||||
public GridViewAdapter(Context context, int layoutResourceId, List<Media> 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<Media> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FeaturedImage> getMockFeaturedImages(){
|
||||
ArrayList<FeaturedImage> 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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<FeaturedImage> data = new ArrayList();
|
||||
|
||||
public MockGridViewAdapter(Context context, int layoutResourceId, ArrayList<FeaturedImage> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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,20 +307,30 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
}
|
||||
rebuildCatList();
|
||||
|
||||
if(media.getCreator() == null || media.getCreator().equals("")) {
|
||||
authorLayout.setVisibility(GONE);
|
||||
} else {
|
||||
author.setText(media.getCreator());
|
||||
}
|
||||
|
||||
checkDeletion(media);
|
||||
}
|
||||
|
||||
private void setOnClickListeners(final Media media) {
|
||||
if (licenseLink(media) != null) {
|
||||
license.setOnClickListener(v -> openWebBrowser(licenseLink(media)));
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Media> getCategoryImages(String categoryName) {
|
||||
ApiResult apiResult = null;
|
||||
try {
|
||||
MWApi.RequestBuilder requestBuilder = api.action("query")
|
||||
.param("generator", "categorymembers")
|
||||
.param("format", "xml")
|
||||
.param("gcmtype", "file")
|
||||
.param("gcmtitle", categoryName)
|
||||
.param("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")
|
||||
|
|
|
|||
|
|
@ -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<Media> getCategoryImages(String categoryName);
|
||||
|
||||
@NonNull
|
||||
UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener) throws IOException;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
|
|
@ -16,20 +15,12 @@
|
|||
android:layout_height="wrap_content" />
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/featuredFragmentContainer"
|
||||
android:id="@+id/fragmentContainer"
|
||||
android:orientation="horizontal"
|
||||
android:layout_below="@id/toolbar">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/featuedListFragment"
|
||||
android:name="fr.free.nrw.commons.featured.FeaturedImagesListFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:layout="@layout/fragment_contributions" />
|
||||
|
||||
</FrameLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
|
|
@ -1,20 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/mainBackground"
|
||||
>
|
||||
android:background="?attr/mainBackground">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/waiting_first_sync"
|
||||
android:id="@+id/waitingMessage"
|
||||
android:id="@+id/statusMessage"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_centerVertical="true"
|
||||
/>
|
||||
|
||||
<ProgressBar
|
||||
|
|
@ -22,11 +23,11 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:visibility="gone"
|
||||
android:id="@+id/loadingFeaturedImagesProgressBar"
|
||||
android:id="@+id/loadingImagesProgressBar"
|
||||
/>
|
||||
|
||||
<GridView
|
||||
android:id="@+id/featuredImagesList"
|
||||
android:id="@+id/categoryImagesList"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:stretchMode="columnWidth"
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/featuredImagesSequenceNumber"
|
||||
android:id="@+id/categoryImagesSequenceNumber"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="98sp"
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
/>
|
||||
|
||||
<fr.free.nrw.commons.MediaWikiImageView
|
||||
android:id="@+id/featuredImageView"
|
||||
android:id="@+id/categoryImageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="240dp"
|
||||
/>
|
||||
|
|
@ -33,7 +33,7 @@
|
|||
android:padding="@dimen/small_gap"
|
||||
>
|
||||
<ProgressBar
|
||||
android:id="@+id/featuredProgress"
|
||||
android:id="@+id/categoryProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/ProgressBar"
|
||||
|
|
@ -43,7 +43,7 @@
|
|||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/featuredImageTitle"
|
||||
android:id="@+id/categoryImageTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="#FFFFFFFF"
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/featuredImageAuthor"
|
||||
android:id="@+id/categoryImageAuthor"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="#FFFFFFFF"
|
||||
|
|
@ -270,5 +270,9 @@
|
|||
<string name="about_translate_proceed">Proceed</string>
|
||||
<string name="about_translate_cancel">Cancel</string>
|
||||
<string name="retry">Retry</string>
|
||||
|
||||
<string name="no_images_found">No images found!</string>
|
||||
<string name="error_loading_images">Error occurred while loading images.</string>
|
||||
<string name="image_uploaded_by">Uploaded by: %1$s</string>
|
||||
<string name="share_app_title">Share App</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -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<String, String> = 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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 + "/")
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue