mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 06:43:56 +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
				
			
		|  | @ -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,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); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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")); | ||||
|     } | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Vivek Maskara
						Vivek Maskara