Feature/refactor contributions (#3046)

* * Refactored ContributionsListFragment to use RecyclerView
* Added ContributionsPresenter
* Extracted out the cursor to presenter
* Probable fix for #3028

* Improved the logic for cache in ContributionViewHolder

* Some more refactoring

* While displaying images in ContributionsList, check if status is not completed && local uri exists, use that uri to show image

* typo correction in LocalDataSource

* Fixed formatting in ContributionsPresenter

* retain adapter position when orientation changes

* retain child position with its id

* Made ContributionViewHolder not implement ViewHolder

* Code formatting, review suggested changes

* initialise the rv layout managers only when needed

* added test cases for ContributionPresenter

* removed not needed semi colon

* added more java docs and code formatting
This commit is contained in:
Ashish Kumar 2019-07-07 12:54:28 +05:30 committed by neslihanturan
parent 108e28c89a
commit 60b1eb1957
22 changed files with 814 additions and 592 deletions

View file

@ -135,31 +135,4 @@ public class BookmarksActivity extends NavigationBaseActivity
}
return adapter.getMediaAdapter().getCount();
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void notifyDatasetChanged() {
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void registerDataSetObserver(DataSetObserver observer) {
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
}
}

View file

@ -173,32 +173,6 @@ public class CategoryDetailsActivity extends NavigationBaseActivity
return categoryImagesListFragment.getAdapter().getCount();
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void notifyDatasetChanged() {
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void registerDataSetObserver(DataSetObserver observer) {
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
}
/**
* This method inflates the menu in the toolbar
*/

View file

@ -180,33 +180,6 @@ public class CategoryImagesActivity
return categoryImagesListFragment.getAdapter().getCount();
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void notifyDatasetChanged() {
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void registerDataSetObserver(DataSetObserver observer) {
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
}
/**
* This method inflates the menu in the toolbar
*/

View file

@ -1,35 +1,34 @@
package fr.free.nrw.commons.contributions;
import android.content.Context;
import android.net.Uri;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.collection.LruCache;
import com.facebook.drawee.view.SimpleDraweeView;
import org.apache.commons.lang3.StringUtils;
import javax.inject.Inject;
import javax.inject.Named;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.facebook.drawee.view.SimpleDraweeView;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.ViewHolder;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.contributions.model.DisplayableContribution;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.upload.FileUtils;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import timber.log.Timber;
public class ContributionViewHolder implements ViewHolder<DisplayableContribution> {
public class ContributionViewHolder extends RecyclerView.ViewHolder {
private final Callback callback;
@BindView(R.id.contributionImage)
SimpleDraweeView imageView;
@BindView(R.id.contributionTitle) TextView titleView;
@ -47,15 +46,18 @@ public class ContributionViewHolder implements ViewHolder<DisplayableContributio
private DisplayableContribution contribution;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private int position;
ContributionViewHolder(View parent) {
ContributionViewHolder(View parent, Callback callback) {
super(parent);
ButterKnife.bind(this, parent);
this.callback=callback;
}
@Override
public void bindModel(Context context, DisplayableContribution contribution) {
ApplicationlessInjection.getInstance(context)
public void init(int position, DisplayableContribution contribution) {
ApplicationlessInjection.getInstance(itemView.getContext())
.getCommonsApplicationComponent().inject(this);
this.position=position;
this.contribution = contribution;
fetchAndDisplayThumbnail(contribution);
titleView.setText(contribution.getDisplayTitle());
@ -104,19 +106,39 @@ public class ContributionViewHolder implements ViewHolder<DisplayableContributio
* @param contribution
*/
private void fetchAndDisplayThumbnail(DisplayableContribution contribution) {
if (!StringUtils.isBlank(thumbnailCache.get(contribution.getFilename()))) {
imageView.setImageURI(thumbnailCache.get(contribution.getFilename()));
String keyForLRUCache = getKeyForLRUCache(contribution.getContentUri());
String cacheUrl = thumbnailCache.get(keyForLRUCache);
if (!StringUtils.isBlank(cacheUrl)) {
imageView.setImageURI(cacheUrl);
return;
}
Timber.d("Fetching thumbnail for %s", contribution.getFilename());
Disposable disposable = mediaDataExtractor.getMediaFromFileName(contribution.getFilename())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> {
thumbnailCache.put(contribution.getFilename(), media.getThumbUrl());
imageView.setImageURI(media.getThumbUrl());
});
compositeDisposable.add(disposable);
imageView.setBackground(null);
if ((contribution.getState() != Contribution.STATE_COMPLETED) && FileUtils.fileExists(
contribution.getLocalUri())) {
imageView.setImageURI(contribution.getLocalUri());
} else {
Timber.d("Fetching thumbnail for %s", contribution.getFilename());
Disposable disposable = mediaDataExtractor
.getMediaFromFileName(contribution.getFilename())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> {
thumbnailCache.put(keyForLRUCache, media.getThumbUrl());
imageView.setImageURI(media.getThumbUrl());
});
compositeDisposable.add(disposable);
}
}
/**
* Returns image key for the LRU cache, basically the id of the image, (the content uri is the ""+/id)
* @param contentUri
* @return
*/
private String getKeyForLRUCache(Uri contentUri) {
return contentUri.getLastPathSegment();
}
public void clear() {
@ -128,10 +150,7 @@ public class ContributionViewHolder implements ViewHolder<DisplayableContributio
*/
@OnClick(R.id.retryButton)
public void retryUpload() {
DisplayableContribution.ContributionActions actions = contribution.getContributionActions();
if (actions != null) {
actions.retryUpload();
}
callback.retryUpload(contribution);
}
/**
@ -139,17 +158,11 @@ public class ContributionViewHolder implements ViewHolder<DisplayableContributio
*/
@OnClick(R.id.cancelButton)
public void deleteUpload() {
DisplayableContribution.ContributionActions actions = contribution.getContributionActions();
if (actions != null) {
actions.deleteUpload();
}
callback.deleteUpload(contribution);
}
@OnClick(R.id.contributionImage)
public void imageClicked(){
DisplayableContribution.ContributionActions actions = contribution.getContributionActions();
if (actions != null) {
actions.onClick();
}
callback.openMediaDetail(position);
}
}

View file

@ -0,0 +1,35 @@
package fr.free.nrw.commons.contributions;
import android.database.Cursor;
import androidx.loader.app.LoaderManager;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.Media;
/**
* The contract for Contributions View & Presenter
*/
public class ContributionsContract {
public interface View {
void showWelcomeTip(boolean numberOfUploads);
void showProgress(boolean shouldShow);
void showNoContributionsUI(boolean shouldShow);
void setUploadCount(int count);
void onDataSetChanged();
}
public interface UserActionListener extends BasePresenter<ContributionsContract.View>,
LoaderManager.LoaderCallbacks<Cursor> {
Contribution getContributionsFromCursor(Cursor cursor);
void deleteUpload(Contribution contribution);
Media getItemAtPosition(int i);
}
}

View file

@ -1,36 +1,28 @@
package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
import android.Manifest;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Bundle;
import android.os.IBinder;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Adapter;
import android.widget.CheckBox;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.cursoradapter.widget.CursorAdapter;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
import androidx.fragment.app.FragmentTransaction;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import java.util.ArrayList;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.HandlerService;
@ -40,12 +32,15 @@ import fr.free.nrw.commons.campaigns.Campaign;
import fr.free.nrw.commons.campaigns.CampaignView;
import fr.free.nrw.commons.campaigns.CampaignsPresenter;
import fr.free.nrw.commons.campaigns.ICampaignsView;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.contributions.ContributionsListFragment.SourceRefresher;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.location.LocationUpdateListener;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.nearby.NearbyController;
@ -55,29 +50,26 @@ import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.UploadService;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
import static fr.free.nrw.commons.contributions.MainActivity.CONTRIBUTIONS_TAB_POSITION;
import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
public class ContributionsFragment
extends CommonsDaggerSupportFragment
implements LoaderManager.LoaderCallbacks<Cursor>,
MediaDetailPagerFragment.MediaDetailProvider,
FragmentManager.OnBackStackChangedListener,
ContributionsListFragment.SourceRefresher,
LocationUpdateListener,
ICampaignsView,
ContributionsListAdapter.EventListener{
implements
MediaDetailProvider,
OnBackStackChangedListener,
SourceRefresher,
LocationUpdateListener,
ICampaignsView, ContributionsContract.View {
@Inject @Named("default_preferences") JsonKvStore store;
@Inject ContributionDao contributionDao;
@Inject MediaWikiApi mediaWikiApi;
@ -99,6 +91,8 @@ public class ContributionsFragment
@BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView;
@BindView(R.id.campaigns_view) CampaignView campaignView;
@Inject ContributionsPresenter contributionsPresenter;
private LatLng curLatLng;
private boolean firstLocationUpdate = true;
@ -116,9 +110,6 @@ public class ContributionsFragment
uploadService = (UploadService) ((HandlerService.HandlerServiceLocalBinder) binder)
.getService();
isUploadServiceConnected = true;
if (contributionsListFragment.getAdapter() != null) {
((ContributionsListAdapter)contributionsListFragment.getAdapter()).setUploadService(uploadService);
}
}
@Override
@ -127,6 +118,8 @@ public class ContributionsFragment
Timber.e(new RuntimeException("UploadService died but the rest of the process did not!"));
}
};
private boolean shouldShowMediaDetailsFragment;
private int numberOfContributions;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
@ -140,6 +133,7 @@ public class ContributionsFragment
View view = inflater.inflate(R.layout.fragment_contributions, container, false);
ButterKnife.bind(this, view);
presenter.onAttachView(this);
contributionsPresenter.onAttachView(this);
campaignView.setVisibility(View.GONE);
checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
@ -151,16 +145,19 @@ public class ContributionsFragment
});
if (savedInstanceState != null) {
mediaDetailPagerFragment = (MediaDetailPagerFragment)getChildFragmentManager().findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
contributionsListFragment = (ContributionsListFragment) getChildFragmentManager().findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG);
mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager()
.findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
contributionsListFragment = (ContributionsListFragment) getChildFragmentManager()
.findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG);
shouldShowMediaDetailsFragment = savedInstanceState.getBoolean("mediaDetailsVisible");
}
if (savedInstanceState.getBoolean("mediaDetailsVisible")) {
setMediaDetailPagerFragment();
} else {
setContributionsListFragment();
}
} else {
setContributionsListFragment();
initFragments();
if(shouldShowMediaDetailsFragment){
showMediaDetailPagerFragment();
}else{
showContributionsListFragment();
}
if (!ConfigUtils.isBetaFlavour()) {
@ -191,6 +188,65 @@ public class ContributionsFragment
return view;
}
/**
* Initialose the ContributionsListFragment and MediaDetailPagerFragment fragment
*/
private void initFragments() {
if (null == contributionsListFragment) {
contributionsListFragment = new ContributionsListFragment();
}
contributionsListFragment.setCallback(new Callback() {
@Override
public void retryUpload(Contribution contribution) {
ContributionsFragment.this.retryUpload(contribution);
}
@Override
public void deleteUpload(Contribution contribution) {
contributionsPresenter.deleteUpload(contribution);
}
@Override
public void openMediaDetail(int position) {
showDetail(position);
}
@Override
public int getNumberOfContributions() {
return numberOfContributions;
}
@Override
public Contribution getContributionForPosition(int position) {
return (Contribution) contributionsPresenter.getItemAtPosition(position);
}
@Override
public int findItemPositionWithId(String id) {
return contributionsPresenter.getChildPositionWithId(id);
}
});
if(null==mediaDetailPagerFragment){
mediaDetailPagerFragment=new MediaDetailPagerFragment();
}
}
/**
* Replaces the root frame layout with the given fragment
* @param fragment
* @param tag
*/
private void showFragment(Fragment fragment, String tag) {
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.replace(R.id.root_frame, fragment, tag);
transaction.addToBackStack(CONTRIBUTION_LIST_FRAGMENT_TAG);
transaction.commit();
getChildFragmentManager().executePendingTransactions();
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
@ -209,132 +265,45 @@ public class ContributionsFragment
}
/**
* Replace FrameLayout with ContributionsListFragment, user will see contributions list.
* Creates new one if null.
* Replace FrameLayout with ContributionsListFragment, user will see contributions list. Creates
* new one if null.
*/
public void setContributionsListFragment() {
public void showContributionsListFragment() {
// show tabs on contribution list is visible
((MainActivity)getActivity()).showTabs();
((MainActivity) getActivity()).showTabs();
// show nearby card view on contributions list is visible
if (nearbyNotificationCardView != null) {
if (store.getBoolean("displayNearbyCardView", true)) {
if (nearbyNotificationCardView.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) {
if (nearbyNotificationCardView.cardViewVisibilityState
== NearbyNotificationCardView.CardViewVisibilityState.READY) {
nearbyNotificationCardView.setVisibility(View.VISIBLE);
}
} else {
nearbyNotificationCardView.setVisibility(View.GONE);
}
}
// Create if null
if (getContributionsListFragment() == null) {
contributionsListFragment = new ContributionsListFragment();
}
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
// When this container fragment is created, we fill it with our ContributionsListFragment
transaction.replace(R.id.root_frame, contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG);
transaction.addToBackStack(CONTRIBUTION_LIST_FRAGMENT_TAG);
transaction.commit();
getChildFragmentManager().executePendingTransactions();
showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG);
}
/**
* Replace FrameLayout with MediaDetailPagerFragment, user will see details of selected media.
* Creates new one if null.
*/
public void setMediaDetailPagerFragment() {
public void showMediaDetailPagerFragment() {
// hide tabs on media detail view is visible
((MainActivity)getActivity()).hideTabs();
// hide nearby card view on media detail is visible
nearbyNotificationCardView.setVisibility(View.GONE);
// Create if null
if (getMediaDetailPagerFragment() == null) {
mediaDetailPagerFragment = new MediaDetailPagerFragment();
}
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
// When this container fragment is created, we fill it with our MediaDetailPagerFragment
//transaction.addToBackStack(null);
transaction.add(R.id.root_frame, mediaDetailPagerFragment, MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
transaction.addToBackStack(MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
transaction.commit();
getChildFragmentManager().executePendingTransactions();
showFragment(mediaDetailPagerFragment,MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
}
/**
* Just getter method of ContributionsListFragment child of ContributionsFragment
* @return contributionsListFragment, if any created
*/
public ContributionsListFragment getContributionsListFragment() {
return contributionsListFragment;
}
/**
* Just getter method of MediaDetailPagerFragment child of ContributionsFragment
* @return mediaDetailsFragment, if any created
*/
public MediaDetailPagerFragment getMediaDetailPagerFragment() {
return mediaDetailPagerFragment;
}
@Override
public void onBackStackChanged() {
((MainActivity)getActivity()).initBackButton();
}
@Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
int uploads = store.getInt(UPLOADS_SHOWING, 100);
return new CursorLoader(getActivity(), BASE_URI, //TODO find out the reason we pass activity here
ALL_FIELDS, "", null,
ContributionDao.CONTRIBUTION_SORT + "LIMIT " + uploads);
}
@Override
public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
if (contributionsListFragment != null) {
contributionsListFragment.changeProgressBarVisibility(false);
if (contributionsListFragment.getAdapter() == null) {
contributionsListFragment.setAdapter(new ContributionsListAdapter(getActivity().getApplicationContext(),
cursor, 0, contributionDao, this));
} else {
((CursorAdapter) contributionsListFragment.getAdapter()).swapCursor(cursor);
}
contributionsListFragment.showWelcomeTip(cursor.getCount() == 0);
notifyAndMigrateDataSetObservers();
((ContributionsListAdapter)contributionsListFragment.getAdapter()).setUploadService(uploadService);
}
}
@Override
public void onLoaderReset(Loader<Cursor> cursorLoader) {
((CursorAdapter) contributionsListFragment.getAdapter()).swapCursor(null);
}
private void notifyAndMigrateDataSetObservers() {
Adapter adapter = contributionsListFragment.getAdapter();
// First, move the observers over to the adapter now that we have it.
for (DataSetObserver observer : observersWaitingForLoad) {
adapter.registerDataSetObserver(observer);
}
observersWaitingForLoad.clear();
// Now fire off a first notification...
for (DataSetObserver observer : observersWaitingForLoad) {
observer.onChanged();
}
if (ConfigUtils.isBetaFlavour()) {
betaSetUploadCount(getTotalMediaCount());
} else {
setUploadCount();
}
}
/**
* Called when onAuthCookieAcquired is called on authenticated parent activity
* @param uploadServiceIntent
@ -345,7 +314,7 @@ public class ContributionsFragment
if (getActivity() != null) { // If fragment is attached to parent activity
getActivity().bindService(uploadServiceIntent, uploadServiceConnection, Context.BIND_AUTO_CREATE);
isUploadServiceConnected = true;
getActivity().getSupportLoaderManager().initLoader(0, null, ContributionsFragment.this);
getActivity().getSupportLoaderManager().initLoader(0, null, contributionsPresenter);
}
}
@ -358,57 +327,24 @@ public class ContributionsFragment
public void showDetail(int i) {
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
mediaDetailPagerFragment = new MediaDetailPagerFragment();
setMediaDetailPagerFragment();
showMediaDetailPagerFragment();
}
mediaDetailPagerFragment.showImage(i);
}
@Override
public void refreshSource() {
getActivity().getSupportLoaderManager().restartLoader(0, null, this);
getActivity().getSupportLoaderManager().restartLoader(0, null, contributionsPresenter);
}
@Override
public Media getMediaAtPosition(int i) {
if (contributionsListFragment.getAdapter() == null) {
// not yet ready to return data
return null;
} else {
return contributionDao.fromCursor((Cursor) contributionsListFragment.getAdapter().getItem(i));
}
return contributionsPresenter.getItemAtPosition(i);
}
@Override
public int getTotalMediaCount() {
if (contributionsListFragment.getAdapter() == null) {
return 0;
}
return contributionsListFragment.getAdapter().getCount();
}
@Override
public void notifyDatasetChanged() {
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
Adapter adapter = contributionsListFragment.getAdapter();
if (adapter == null) {
observersWaitingForLoad.add(observer);
} else {
adapter.registerDataSetObserver(observer);
}
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
Adapter adapter = contributionsListFragment.getAdapter();
if (adapter == null) {
observersWaitingForLoad.remove(observer);
} else {
adapter.unregisterDataSetObserver(observer);
}
return numberOfContributions;
}
@SuppressWarnings("ConstantConditions")
@ -454,7 +390,7 @@ public class ContributionsFragment
@Override
public void onResume() {
super.onResume();
contributionsPresenter.onAttachView(this);
firstLocationUpdate = true;
locationManager.addLocationListener(this);
@ -624,11 +560,48 @@ public class ContributionsFragment
}
@Override
public void onEvent(String filename) {
for (int i=0;i<getTotalMediaCount();i++){
if (getMediaAtPosition(i).getFilename().equals(filename))
showDetail(i);
public void showWelcomeTip(boolean shouldShow) {
contributionsListFragment.showWelcomeTip(shouldShow);
}
@Override
public void showProgress(boolean shouldShow) {
contributionsListFragment.showProgress(shouldShow);
}
@Override
public void showNoContributionsUI(boolean shouldShow) {
contributionsListFragment.showNoContributionsUI(shouldShow);
}
@Override
public void setUploadCount(int count) {
this.numberOfContributions=count;
}
@Override
public void onDataSetChanged() {
contributionsListFragment.onDataSetChanged();
mediaDetailPagerFragment.onDataSetChanged();
}
/**
* Retry upload when it is failed
*
* @param contribution contribution to be retried
*/
private void retryUpload(Contribution contribution) {
if (NetworkUtils.isInternetConnectionEstablished(getContext())) {
if (contribution.getState() == STATE_FAILED && null != uploadService) {
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, contribution);
Timber.d("Restarting for %s", contribution.toString());
} else {
Timber.d("Skipping re-upload for non-failed %s", contribution.toString());
}
} else {
ViewUtil.showLongToast(getContext(), R.string.this_function_needs_network_connection);
}
}
}

View file

@ -1,121 +1,55 @@
package fr.free.nrw.commons.contributions;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import androidx.cursoradapter.widget.CursorAdapter;
import android.database.DataSetObserver;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.model.DisplayableContribution;
import fr.free.nrw.commons.upload.UploadService;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber;
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionViewHolder> {
class ContributionsListAdapter extends CursorAdapter {
private Callback callback;
private final ContributionDao contributionDao;
private UploadService uploadService;
public ContributionsListAdapter(Context context,
Cursor c,
int flags,
ContributionDao contributionDao, EventListener listener) {
super(context, c, flags);
this.contributionDao = contributionDao;
this.listener=listener;
public ContributionsListAdapter(Callback callback) {
this.callback = callback;
}
public void setUploadService(UploadService uploadService) {
this.uploadService = uploadService;
@NonNull
@Override
public ContributionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ContributionViewHolder viewHolder = new ContributionViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.layout_contribution, parent, false), callback);
return viewHolder;
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup viewGroup) {
View parent = LayoutInflater.from(context)
.inflate(R.layout.layout_contribution, viewGroup, false);
parent.setTag(new ContributionViewHolder(parent));
return parent;
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
final ContributionViewHolder views = (ContributionViewHolder)view.getTag();
final Contribution contribution = contributionDao.fromCursor(cursor);
Timber.d("Cursor position is %d", cursor.getPosition());
public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) {
final Contribution contribution = callback.getContributionForPosition(position);
DisplayableContribution displayableContribution = new DisplayableContribution(contribution,
cursor.getPosition(),
new DisplayableContribution.ContributionActions() {
@Override
public void retryUpload() {
ContributionsListAdapter.this.retryUpload(view.getContext(), contribution);
}
@Override
public void deleteUpload() {
ContributionsListAdapter.this.deleteUpload(view.getContext(), contribution);
}
@Override
public void onClick() {
ContributionsListAdapter.this.openMediaDetail(contribution);
}
});
views.bindModel(context, displayableContribution);
position);
holder.init(position, displayableContribution);
}
/**
* Retry upload when it is failed
* @param contribution contribution to be retried
*/
private void retryUpload(@NonNull Context context, Contribution contribution) {
if (NetworkUtils.isInternetConnectionEstablished(context)) {
if (contribution.getState() == STATE_FAILED
&& uploadService!= null) {
uploadService.queue(UploadService.ACTION_UPLOAD_FILE, contribution);
Timber.d("Restarting for %s", contribution.toString());
} else {
Timber.d("Skipping re-upload for non-failed %s", contribution.toString());
}
} else {
ViewUtil.showLongToast(context, R.string.this_function_needs_network_connection);
}
@Override
public int getItemCount() {
return callback.getNumberOfContributions();
}
/**
* Delete a failed upload attempt
* @param contribution contribution to be deleted
*/
private void deleteUpload(@NonNull Context context, Contribution contribution) {
if (NetworkUtils.isInternetConnectionEstablished(context)) {
if (contribution.getState() == STATE_FAILED) {
Timber.d("Deleting failed contrib %s", contribution.toString());
contributionDao.delete(contribution);
} else {
Timber.d("Skipping deletion for non-failed contrib %s", contribution.toString());
}
} else {
ViewUtil.showLongToast(context, R.string.this_function_needs_network_connection);
}
public interface Callback {
}
void retryUpload(Contribution contribution);
private void openMediaDetail(Contribution contribution){
listener.onEvent(contribution.getFilename());
void deleteUpload(Contribution contribution);
}
EventListener listener;
void openMediaDetail(int contribution);
public interface EventListener {
void onEvent(String filename);
int getNumberOfContributions();
Contribution getContributionForPosition(int position);
int findItemPositionWithId(String lastVisibleItemID);
}
}

View file

@ -1,34 +1,33 @@
package fr.free.nrw.commons.contributions;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.content.res.Configuration;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ListAdapter;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.content.res.Configuration;
import android.widget.LinearLayout;
import javax.inject.Inject;
import javax.inject.Named;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.LayoutManager;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.utils.ConfigUtils;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import javax.inject.Inject;
import javax.inject.Named;
/**
* Created by root on 01.06.2018.
@ -36,8 +35,9 @@ import static android.view.View.VISIBLE;
public class ContributionsListFragment extends CommonsDaggerSupportFragment {
private static final String VISIBLE_ITEM_ID = "visible_item_id";
@BindView(R.id.contributionsList)
GridView contributionsList;
RecyclerView rvContributionsList;
@BindView(R.id.loadingContributionsProgressBar)
ProgressBar progressBar;
@BindView(R.id.fab_plus)
@ -62,29 +62,56 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
private boolean isFabOpen = false;
private ContributionsListAdapter adapter;
private Callback callback;
private String lastVisibleItemID;
private int SPAN_COUNT=3;
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
ButterKnife.bind(this, view);
changeProgressBarVisibility(true);
initAdapter();
return view;
}
public void setCallback(Callback callback) {
this.callback = callback;
}
private void initAdapter() {
adapter = new ContributionsListAdapter(callback);
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initRecyclerView();
initializeAnimations();
setListeners();
}
private void initRecyclerView() {
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
rvContributionsList.setLayoutManager(new GridLayoutManager(getContext(),SPAN_COUNT));
} else {
rvContributionsList.setLayoutManager(new LinearLayoutManager(getContext()));
}
rvContributionsList.setAdapter(adapter);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// check orientation
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
fab_layout.setOrientation(LinearLayout.HORIZONTAL);
rvContributionsList.setLayoutManager(new GridLayoutManager(getContext(),SPAN_COUNT));
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
fab_layout.setOrientation(LinearLayout.VERTICAL);
rvContributionsList.setLayoutManager(new LinearLayoutManager(getContext()));
}
}
@ -127,39 +154,70 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
}
}
/**
* Responsible to set progress bar invisible and visible
* @param isVisible True when contributions list should be hidden.
*/
public void changeProgressBarVisibility(boolean isVisible) {
this.progressBar.setVisibility(isVisible ? VISIBLE : GONE);
}
/**
* Shows welcome message if user has no contributions yet i.e. new user.
*/
protected void showWelcomeTip(boolean noContributions) {
noContributionsYet.setVisibility(noContributions ? VISIBLE : GONE);
}
public ListAdapter getAdapter() {
return contributionsList.getAdapter();
public void showWelcomeTip(boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
/**
* Sets adapter to contributions list. If beta mode, sets upload count for beta explicitly.
* @param adapter List adapter for uploads of contributor
* Responsible to set progress bar invisible and visible
* @param shouldShow True when contributions list should be hidden.
*/
public void setAdapter(ListAdapter adapter) {
this.contributionsList.setAdapter(adapter);
public void showProgress(boolean shouldShow) {
progressBar.setVisibility(shouldShow ? VISIBLE : GONE);
}
if (ConfigUtils.isBetaFlavour()) {
//TODO: add betaSetUploadCount method
((ContributionsFragment) getParentFragment()).betaSetUploadCount(adapter.getCount());
public void showNoContributionsUI(boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow?VISIBLE:GONE);
}
public void onDataSetChanged() {
if (null != adapter) {
adapter.notifyDataSetChanged();
//Restoring last visible item position in cases of orientation change
if (null != lastVisibleItemID) {
int itemPositionWithId = callback.findItemPositionWithId(lastVisibleItemID);
rvContributionsList.scrollToPosition(itemPositionWithId);
lastVisibleItemID = null;//Reset the lastVisibleItemID once we have used it
}
}
}
public interface SourceRefresher {
void refreshSource();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
LayoutManager layoutManager = rvContributionsList.getLayoutManager();
int lastVisibleItemPosition=0;
if(layoutManager instanceof LinearLayoutManager){
lastVisibleItemPosition= ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition();
}else if(layoutManager instanceof GridLayoutManager){
lastVisibleItemPosition=((GridLayoutManager)layoutManager).findLastCompletelyVisibleItemPosition();
}
outState.putString(VISIBLE_ITEM_ID,findIdOfItemWithPosition(lastVisibleItemPosition));
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if(null!=savedInstanceState){
lastVisibleItemID =savedInstanceState.getString(VISIBLE_ITEM_ID, null);
}
}
/**
* Gets the id of the contribution from the db
* @param position
* @return
*/
private String findIdOfItemWithPosition(int position) {
return callback.getContributionForPosition(position).getContentUri().getLastPathSegment();
}
}

View file

@ -0,0 +1,47 @@
package fr.free.nrw.commons.contributions;
import android.database.Cursor;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import javax.inject.Inject;
import javax.inject.Named;
/**
* The LocalDataSource class for Contributions
*/
class ContributionsLocalDataSource {
private final ContributionDao contributionsDao;
private final JsonKvStore defaultKVStore;
@Inject
public ContributionsLocalDataSource(
@Named("default_preferences") JsonKvStore defaultKVStore,
ContributionDao contributionDao) {
this.defaultKVStore = defaultKVStore;
this.contributionsDao = contributionDao;
}
/**
* Fetch default number of contributions to be show, based on user preferences
*/
public int get(String key) {
return defaultKVStore.getInt(key);
}
/**
* Get contribution object from cursor
* @param cursor
* @return
*/
public Contribution getContributionFromCursor(Cursor cursor) {
return contributionsDao.fromCursor(cursor);
}
/**
* Remove a contribution from the contributions table
* @param contribution
*/
public void deleteContribution(Contribution contribution) {
contributionsDao.delete(contribution);
}
}

View file

@ -0,0 +1,15 @@
package fr.free.nrw.commons.contributions;
import dagger.Binds;
import dagger.Module;
/**
* The Dagger Module for contributions related presenters and (some other objects maybe in future)
*/
@Module
public abstract class ContributionsModule {
@Binds
public abstract ContributionsContract.UserActionListener bindsContibutionsPresenter(
ContributionsPresenter presenter);
}

View file

@ -0,0 +1,180 @@
package fr.free.nrw.commons.contributions;
import static fr.free.nrw.commons.contributions.ContributionDao.Table.ALL_FIELDS;
import static fr.free.nrw.commons.contributions.ContributionsContentProvider.BASE_URI;
import static fr.free.nrw.commons.settings.Prefs.UPLOADS_SHOWING;
import android.content.Context;
import android.database.Cursor;
import android.database.DataSetObserver;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
import javax.inject.Inject;
import timber.log.Timber;
/**
* The presenter class for Contributions
*/
public class ContributionsPresenter extends DataSetObserver implements UserActionListener {
private final ContributionsRepository repository;
private ContributionsContract.View view;
private Cursor cursor;
@Inject
Context context;
@Inject
ContributionsPresenter(ContributionsRepository repository) {
this.repository = repository;
}
@Override
public void onAttachView(ContributionsContract.View view) {
this.view = view;
if (null != cursor) {
try {
cursor.registerDataSetObserver(this);
} catch (IllegalStateException e) {//Cursor might be already registered
Timber.d(e);
}
}
}
@Override
public void onDetachView() {
this.view = null;
if (null != cursor) {
try {
cursor.unregisterDataSetObserver(this);
} catch (Exception e) {//Cursor might not be already registered
Timber.d(e);
}
}
}
@NonNull
@Override
public Loader<Cursor> onCreateLoader(int id, @Nullable Bundle args) {
int preferredNumberOfUploads = repository.get(UPLOADS_SHOWING);
return new CursorLoader(context, BASE_URI,
ALL_FIELDS, "", null,
ContributionDao.CONTRIBUTION_SORT + "LIMIT "
+ (preferredNumberOfUploads>0?preferredNumberOfUploads:100));
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor cursor) {
view.showProgress(false);
if (null != cursor && cursor.getCount() > 0) {
view.showWelcomeTip(false);
view.showNoContributionsUI(false);
view.setUploadCount(cursor.getCount());
} else {
view.showWelcomeTip(true);
view.showNoContributionsUI(true);
}
swapCursor(cursor);
}
@Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
this.cursor = null;
//On LoadFinished is not guaranteed to be called
view.showProgress(false);
view.showWelcomeTip(true);
view.showNoContributionsUI(true);
swapCursor(null);
}
/**
* Get contribution from the repository
*/
@Override
public Contribution getContributionsFromCursor(Cursor cursor) {
return repository.getContributionFromCursor(cursor);
}
/**
* Delete a failed contribution from the local db
* @param contribution
*/
@Override
public void deleteUpload(Contribution contribution) {
repository.deleteContributionFromDB(contribution);
}
/**
* Returns a contribution at the specified cursor position
* @param i
* @return
*/
@Nullable
@Override
public Media getItemAtPosition(int i) {
if (null != cursor && cursor.moveToPosition(i)) {
return getContributionsFromCursor(cursor);
}
return null;
}
/**
* Get contribution position with id
*/
public int getChildPositionWithId(String id) {
int position = 0;
cursor.moveToFirst();
while (null != cursor && cursor.moveToNext()) {
if (getContributionsFromCursor(cursor).getContentUri().getLastPathSegment()
.equals(id)) {
position = cursor.getPosition();
break;
}
}
return position;
}
@Override
public void onChanged() {
super.onChanged();
view.onDataSetChanged();
}
@Override
public void onInvalidated() {
super.onInvalidated();
//Not letting the view know of this as of now, TODO discuss how to handle this and maybe show a proper ui for this
}
/**
* Swap in a new Cursor, returning the old Cursor. The returned old Cursor is <em>not</em>
* closed.
*
* @param newCursor The new cursor to be used.
* @return Returns the previously set Cursor, or null if there was not one. If the given new
* Cursor is the same instance is the previously set Cursor, null is also returned.
*/
private void swapCursor(Cursor newCursor) {
try {
if (newCursor == cursor) {
return;
}
Cursor oldCursor = cursor;
if (oldCursor != null) {
oldCursor.unregisterDataSetObserver(this);
}
cursor = newCursor;
if (newCursor != null) {
newCursor.registerDataSetObserver(this);
}
view.onDataSetChanged();
} catch (IllegalStateException e) {//Cursor might [not] be already registered/unregistered
Timber.e(e);
}
}
}

View file

@ -0,0 +1,42 @@
package fr.free.nrw.commons.contributions;
import android.database.Cursor;
import javax.inject.Inject;
/**
* The repository class for contributions
*/
public class ContributionsRepository {
private ContributionsLocalDataSource localDataSource;
@Inject
public ContributionsRepository(ContributionsLocalDataSource localDataSource) {
this.localDataSource = localDataSource;
}
/**
* Fetch default number of contributions to be show, based on user preferences
*/
public int get(String uploadsShowing) {
return localDataSource.get(uploadsShowing);
}
/**
* Get contribution object from cursor from LocalDataSource
* @param cursor
* @return
*/
public Contribution getContributionFromCursor(Cursor cursor) {
return localDataSource.getContributionFromCursor(cursor);
}
/**
* Deletes a failed upload from DB
* @param contribution
*/
public void deleteContributionFromDB(Contribution contribution) {
localDataSource.deleteContribution(contribution);
}
}

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons.contributions;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
@ -385,13 +384,6 @@ public class MainActivity extends AuthenticatedActivity implements FragmentManag
case 0:
ContributionsFragment retainedContributionsFragment = getContributionsFragment(0);
if (retainedContributionsFragment != null) {
// ContributionsFragment is parent of ContributionsListFragment and
// MediaDetailsFragment. If below decides which child will be visible.
if (isContributionsListFragment) {
retainedContributionsFragment.setContributionsListFragment();
} else {
retainedContributionsFragment.setMediaDetailPagerFragment();
}
return retainedContributionsFragment;
} else {
// If we reach here, retainedContributionsFragment is null

View file

@ -4,9 +4,7 @@ import fr.free.nrw.commons.contributions.Contribution;
public class DisplayableContribution extends Contribution {
private int position;
private ContributionActions contributionActions;
private DisplayableContribution(Contribution contribution,
public DisplayableContribution(Contribution contribution,
int position) {
super(contribution.getContentUri(),
contribution.getFilename(),
@ -27,13 +25,6 @@ public class DisplayableContribution extends Contribution {
this.position = position;
}
public DisplayableContribution(Contribution contribution,
int position,
ContributionActions contributionActions) {
this(contribution, position);
this.contributionActions = contributionActions;
}
public int getPosition() {
return position;
}
@ -41,16 +32,4 @@ public class DisplayableContribution extends Contribution {
public void setPosition(int position) {
this.position = position;
}
public ContributionActions getContributionActions() {
return contributionActions;
}
public interface ContributionActions {
void retryUpload();
void deleteUpload();
void onClick();
}
}

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.di;
import fr.free.nrw.commons.contributions.ContributionsModule;
import javax.inject.Singleton;
import dagger.Component;
@ -28,7 +29,7 @@ import fr.free.nrw.commons.widget.PicOfDayAppWidget;
ActivityBuilderModule.class,
FragmentBuilderModule.class,
ServiceBuilderModule.class,
ContentProviderBuilderModule.class, UploadModule.class
ContentProviderBuilderModule.class, UploadModule.class, ContributionsModule.class
})
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
void inject(CommonsApplication application);

View file

@ -143,14 +143,6 @@ public class SearchActivity extends NavigationBaseActivity implements MediaDetai
return searchImageFragment.getTotalImagesCount();
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void notifyDatasetChanged() {
}
/**
* This method is called on success of API call for image Search.
* The viewpager will notified that number of items have changed.
@ -161,24 +153,6 @@ public class SearchActivity extends NavigationBaseActivity implements MediaDetai
}
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void registerDataSetObserver(DataSetObserver observer) {
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
}
/**
* Open media detail pager fragment on click of image in search results
* @param index item index that should be opened

View file

@ -141,14 +141,6 @@ public class ExploreActivity
}
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void notifyDatasetChanged() {
}
/**
* This method is called on success of API call for featured images or mobile uploads.
* The viewpager will notified that number of items have changed.
@ -159,23 +151,6 @@ public class ExploreActivity
}
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void registerDataSetObserver(DataSetObserver observer) {
}
/**
* This method is never called but it was in MediaDetailProvider Interface
* so it needs to be overrided.
*/
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
}
/**
* This method is called on backPressed of anyFragment in the activity.

View file

@ -1,5 +1,8 @@
package fr.free.nrw.commons.media;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.Intent;
@ -21,25 +24,13 @@ import android.widget.ScrollView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.SimpleDraweeView;
import com.facebook.imagepipeline.request.ImageRequest;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.util.DateUtil;
import org.wikipedia.util.StringUtil;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.R;
@ -57,11 +48,15 @@ import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.util.DateUtil;
import org.wikipedia.util.StringUtil;
import timber.log.Timber;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private boolean editable;
@ -134,7 +129,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private boolean categoriesPresent = false;
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
private ViewTreeObserver.OnScrollChangedListener scrollListener;
private DataSetObserver dataObserver;
//Had to make this class variable, to implement various onClicks, which access the media, also I fell why make separate variables when one can serve the purpose
private Media media;
@ -232,34 +226,15 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@Override
public void onResume() {
super.onResume();
if(getParentFragment()!=null && getParentFragment().getParentFragment()!=null) {
if (getParentFragment() != null && getParentFragment().getParentFragment() != null) {
//Added a check because, not necessarily, the parent fragment will have a parent fragment, say
// in the case when MediaDetailPagerFragment is directly started by the CategoryImagesActivity
((ContributionsFragment) (getParentFragment().getParentFragment())).nearbyNotificationCardView
.setVisibility(View.GONE);
((ContributionsFragment) (getParentFragment()
.getParentFragment())).nearbyNotificationCardView
.setVisibility(View.GONE);
}
media = detailProvider.getMediaAtPosition(index);
if (media == null) {
// Ask the detail provider to ping us when we're ready
Timber.d("MediaDetailFragment not yet ready to display details; registering observer");
dataObserver = new DataSetObserver() {
@Override
public void onChanged() {
if (!isAdded()) {
return;
}
Timber.d("MediaDetailFragment ready to display delayed details!");
detailProvider.unregisterDataSetObserver(dataObserver);
dataObserver = null;
media=detailProvider.getMediaAtPosition(index);
displayMediaDetails();
}
};
detailProvider.registerDataSetObserver(dataObserver);
} else {
Timber.d("MediaDetailFragment ready to display details");
displayMediaDetails();
}
displayMediaDetails();
}
private void displayMediaDetails() {
@ -300,10 +275,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
getView().getViewTreeObserver().removeOnScrollChangedListener(scrollListener);
scrollListener = null;
}
if (dataObserver != null) {
detailProvider.unregisterDataSetObserver(dataObserver);
dataObserver = null;
}
compositeDisposable.clear();
super.onDestroyView();
}

View file

@ -1,9 +1,12 @@
package fr.free.nrw.commons.media;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.content.Context.DOWNLOAD_SERVICE;
import static fr.free.nrw.commons.Utils.handleWebUrl;
import android.annotation.SuppressLint;
import android.app.DownloadManager;
import android.content.Intent;
import android.database.DataSetObserver;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
@ -15,11 +18,6 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import javax.inject.Inject;
import javax.inject.Named;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
@ -44,12 +42,10 @@ import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.PermissionUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.content.Context.DOWNLOAD_SERVICE;
import static fr.free.nrw.commons.Utils.handleWebUrl;
public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener {
@Inject MediaWikiApi mwApi;
@ -351,16 +347,16 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
public void onPageScrollStateChanged(int i) {
}
public void onDataSetChanged() {
if (null != adapter) {
adapter.notifyDataSetChanged();
}
}
public interface MediaDetailProvider {
Media getMediaAtPosition(int i);
int getTotalMediaCount();
void notifyDatasetChanged();
void registerDataSetObserver(DataSetObserver observer);
void unregisterDataSetObserver(DataSetObserver observer);
}
//FragmentStatePagerAdapter allows user to swipe across collection of images (no. of images undetermined)

View file

@ -4,7 +4,7 @@ import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
import android.webkit.MimeTypeMap;
import androidx.exifinterface.media.ExifInterface;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
@ -15,8 +15,6 @@ import java.io.InputStreamReader;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import androidx.exifinterface.media.ExifInterface;
import timber.log.Timber;
public class FileUtils {
@ -164,4 +162,17 @@ public class FileUtils {
}
return true;
}
/**
* Check if file exists in local dirs
*/
public static boolean fileExists(Uri localUri) {
try {
File file = new File(localUri.getPath());
return file.exists();
} catch (Exception e) {
Timber.d(e);
return false;
}
}
}

View file

@ -20,21 +20,16 @@
/>
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:id="@+id/loadingContributionsProgressBar"
/>
<GridView
android:id="@+id/loadingContributionsProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/contributionsList"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:stretchMode="columnWidth"
android:columnWidth="240dp"
android:numColumns="auto_fit"
android:listSelector="@null"
android:fadingEdge="none"
android:fastScrollEnabled="true"
/>
<LinearLayout

View file

@ -0,0 +1,110 @@
package fr.free.nrw.commons.contributions
import android.database.Cursor
import androidx.loader.content.CursorLoader
import androidx.loader.content.Loader
import com.nhaarman.mockito_kotlin.verify
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
/**
* The unit test class for ContributionsPresenter
*/
class ContributionsPresenterTest {
@Mock
internal var repository: ContributionsRepository? = null
@Mock
internal var view: ContributionsContract.View? = null
private var contributionsPresenter: ContributionsPresenter? = null
private lateinit var cursor: Cursor
lateinit var contribution: Contribution
lateinit var loader: Loader<Cursor>
/**
* initial setup
*/
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
cursor = Mockito.mock(Cursor::class.java)
contribution = Mockito.mock(Contribution::class.java)
contributionsPresenter = ContributionsPresenter(repository)
loader = Mockito.mock(CursorLoader::class.java)
contributionsPresenter?.onAttachView(view)
}
/**
* Test presenter actions onGetContributionFromCursor
*/
@Test
fun testGetContributionFromCursor() {
contributionsPresenter?.getContributionsFromCursor(cursor)
verify(repository)?.getContributionFromCursor(cursor)
}
/**
* Test presenter actions onDeleteContribution
*/
@Test
fun testDeleteContribution() {
contributionsPresenter?.deleteUpload(contribution)
verify(repository)?.deleteContributionFromDB(contribution)
}
/**
* Test presenter actions on loaderFinished and has non zero media objects
*/
@Test
fun testOnLoaderFinishedNonZeroContributions() {
Mockito.`when`(cursor.count).thenReturn(1)
contributionsPresenter?.onLoadFinished(loader, cursor)
verify(view)?.showProgress(false)
verify(view)?.showWelcomeTip(false)
verify(view)?.showNoContributionsUI(false)
verify(view)?.setUploadCount(cursor.count)
}
/**
* Test presenter actions on loaderFinished and has Zero media objects
*/
@Test
fun testOnLoaderFinishedZeroContributions() {
Mockito.`when`(cursor.count).thenReturn(0)
contributionsPresenter?.onLoadFinished(loader, cursor)
verify(view)?.showProgress(false)
verify(view)?.showWelcomeTip(true)
verify(view)?.showNoContributionsUI(true)
}
/**
* Test presenter actions on loader reset
*/
@Test
fun testOnLoaderReset() {
contributionsPresenter?.onLoaderReset(loader)
verify(view)?.showProgress(false)
verify(view)?.showWelcomeTip(true)
verify(view)?.showNoContributionsUI(true)
}
/**
* Test presenter actions on loader change
*/
@Test
fun testOnChanged() {
contributionsPresenter?.onChanged()
verify(view)?.onDataSetChanged()
}
}