Fixes #3359 Duplicate Photos in Contributions Page (#3515)

* Fixes #3359
* Cache thumb url & imageUrl in local db
* Use Fresco's ImageRequest to show images in ContributionViewHolder[this was the issue, we should have always used this to show the image]
* Deleted DisplayableContribution (not needed anymore)
* Exposed abstract function in ContributionDao to updateContribution

* * Make position private in ContributionViewHolder
* Remove MediaDataExtractor from ContributionsFragment

* * Show placeholder image for Contributions while the image loads
* setHasStableId's ContributionsAdapter

* make Random variable private in ContributionViewHolder

* replace local variable-if-with ternary operator in ContributionViewHolder

* Fix indentation/formatting of ternary operator in ContributionViewHolder

* I might revert this commit[I have reasons]

* Create in-memory drawables in CVH's onBind, caches are bad, add mental overhead

* Revert "I might revert this commit[I have reasons]"

This reverts commit 627ac91517.

* minor formatting changes, reverted 627ac91517

* uh-oh missed semicolon, java

* minor formatting changes
This commit is contained in:
Ashish Kumar 2020-03-18 16:17:51 +05:30 committed by GitHub
parent 021105ac4d
commit efdb00b5ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 108 additions and 110 deletions

View file

@ -45,6 +45,7 @@ public class Media implements Parcelable {
}; };
// Primary metadata fields // Primary metadata fields
@Nullable
public Uri localUri; public Uri localUri;
public String thumbUrl; public String thumbUrl;
public String imageUrl; public String imageUrl;

View file

@ -8,6 +8,8 @@ import androidx.room.OnConflictStrategy;
import androidx.room.Query; import androidx.room.Query;
import androidx.room.Transaction; import androidx.room.Transaction;
import androidx.room.Update;
import io.reactivex.disposables.Disposable;
import java.util.List; import java.util.List;
import io.reactivex.Completable; import io.reactivex.Completable;
@ -52,4 +54,7 @@ public abstract class ContributionDao {
@Query("Delete FROM contribution WHERE state = :state") @Query("Delete FROM contribution WHERE state = :state")
public abstract void deleteAll(int state); public abstract void deleteAll(int state);
@Update
public abstract Single<Integer> update(Contribution contribution);
} }

View file

@ -1,34 +1,30 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.text.TextUtils;
import android.view.View; import android.view.View;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import androidx.collection.LruCache; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.view.SimpleDraweeView; import com.facebook.drawee.view.SimpleDraweeView;
import org.apache.commons.lang3.StringUtils; import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import javax.inject.Inject;
import javax.inject.Named;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import butterknife.OnClick; import butterknife.OnClick;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback; import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.contributions.model.DisplayableContribution; import java.util.HashMap;
import fr.free.nrw.commons.di.ApplicationlessInjection; import java.util.Random;
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 timber.log.Timber;
public class ContributionViewHolder extends RecyclerView.ViewHolder { public class ContributionViewHolder extends RecyclerView.ViewHolder {
@ -41,16 +37,9 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
@BindView(R.id.contributionProgress) ProgressBar progressView; @BindView(R.id.contributionProgress) ProgressBar progressView;
@BindView(R.id.failed_image_options) LinearLayout failedImageOptions; @BindView(R.id.failed_image_options) LinearLayout failedImageOptions;
@Inject
MediaDataExtractor mediaDataExtractor;
@Inject
@Named("thumbnail-cache")
LruCache<String, String> thumbnailCache;
private DisplayableContribution contribution;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private int position; private int position;
private Contribution contribution;
private Random random = new Random();
ContributionViewHolder(View parent, Callback callback) { ContributionViewHolder(View parent, Callback callback) {
super(parent); super(parent);
@ -58,15 +47,22 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
this.callback=callback; this.callback=callback;
} }
public void init(int position, DisplayableContribution contribution) { public void init(int position, Contribution contribution) {
ApplicationlessInjection.getInstance(itemView.getContext())
.getCommonsApplicationComponent().inject(this);
this.position=position;
this.contribution = contribution; this.contribution = contribution;
fetchAndDisplayThumbnail(contribution); this.position = position;
imageView.getHierarchy().setPlaceholderImage(new ColorDrawable(
Color.argb(100, random.nextInt(256), random.nextInt(256), random.nextInt(256))));
String imageSource = chooseImageSource(contribution.thumbUrl, contribution.getLocalUri());
if (!TextUtils.isEmpty(imageSource)) {
final ImageRequest imageRequest =
ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
.setProgressiveRenderingEnabled(true)
.build();
imageView.setImageRequest(imageRequest);
}
titleView.setText(contribution.getDisplayTitle()); titleView.setText(contribution.getDisplayTitle());
seqNumView.setText(String.valueOf(contribution.getPosition() + 1)); seqNumView.setText(String.valueOf(position + 1));
seqNumView.setVisibility(View.VISIBLE); seqNumView.setVisibility(View.VISIBLE);
switch (contribution.getState()) { switch (contribution.getState()) {
@ -104,40 +100,18 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
} }
/** /**
* This method fetches the thumbnail url from file name * Returns the image source for the image view, first preference is given to thumbUrl if that is
* If the thumbnail url is present in cache, then it is used otherwise API call is made to fetch the thumbnail * null, moves to local uri and if both are null return null
* This can be removed once #2904 is in place and contribution contains all metadata beforehand *
* @param contribution * @param thumbUrl
* @param localUri
* @return
*/ */
private void fetchAndDisplayThumbnail(DisplayableContribution contribution) { @Nullable
String keyForLRUCache = contribution.getFilename(); private String chooseImageSource(String thumbUrl, Uri localUri) {
String cacheUrl = thumbnailCache.get(keyForLRUCache); return !TextUtils.isEmpty(thumbUrl) ? thumbUrl :
if (!StringUtils.isBlank(cacheUrl)) { localUri != null ? localUri.toString() :
imageView.setImageURI(cacheUrl); null;
return;
}
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);
}
}
public void clear() {
compositeDisposable.clear();
} }
/** /**

View file

@ -26,10 +26,15 @@ public class ContributionsContract {
} }
public interface UserActionListener extends BasePresenter<ContributionsContract.View> { public interface UserActionListener extends BasePresenter<ContributionsContract.View> {
Contribution getContributionsWithTitle(String uri); Contribution getContributionsWithTitle(String uri);
void deleteUpload(Contribution contribution); void deleteUpload(Contribution contribution);
Media getItemAtPosition(int i); Media getItemAtPosition(int i);
void updateContribution(Contribution contribution);
void fetchMediaDetails(Contribution contribution);
} }
} }

View file

@ -20,6 +20,8 @@ import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener; import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
import fr.free.nrw.commons.MediaDataExtractor;
import io.reactivex.disposables.Disposable;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
@ -216,6 +218,12 @@ public class ContributionsFragment
public Contribution getContributionForPosition(int position) { public Contribution getContributionForPosition(int position) {
return (Contribution) contributionsPresenter.getItemAtPosition(position); return (Contribution) contributionsPresenter.getItemAtPosition(position);
} }
@Override
public void fetchMediaUriFor(Contribution contribution) {
Timber.d("Fetching thumbnail for %s", contribution.filename);
contributionsPresenter.fetchMediaDetails(contribution);
}
}); });
if(null==mediaDetailPagerFragment){ if(null==mediaDetailPagerFragment){

View file

@ -1,5 +1,8 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -10,7 +13,6 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.model.DisplayableContribution;
/** /**
* Represents The View Adapter for the List of Contributions * Represents The View Adapter for the List of Contributions
@ -22,7 +24,7 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
public ContributionsListAdapter(Callback callback) { public ContributionsListAdapter(Callback callback) {
this.callback = callback; this.callback = callback;
contributions=new ArrayList<>(); contributions = new ArrayList<>();
} }
/** /**
@ -41,9 +43,12 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
@Override @Override
public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) { public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) {
final Contribution contribution = contributions.get(position); final Contribution contribution = contributions.get(position);
DisplayableContribution displayableContribution = new DisplayableContribution(contribution, if (TextUtils.isEmpty(contribution.getThumbUrl())
position); && contribution.getState() == Contribution.STATE_COMPLETED) {
holder.init(position, displayableContribution); callback.fetchMediaUriFor(contribution);
}
holder.init(position, contribution);
} }
@Override @Override
@ -51,12 +56,14 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
return contributions.size(); return contributions.size();
} }
public void setContributions(List<Contribution> contributionList) { public void setContributions(@NonNull List<Contribution> contributionList) {
if(null!=contributionList) { contributions = contributionList;
this.contributions.clear(); notifyDataSetChanged();
this.contributions.addAll(contributionList); }
notifyDataSetChanged();
} @Override
public long getItemId(int position) {
return contributions.get(position)._id;
} }
public interface Callback { public interface Callback {
@ -68,5 +75,7 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
void openMediaDetail(int contribution); void openMediaDetail(int contribution);
Contribution getContributionForPosition(int position); Contribution getContributionForPosition(int position);
void fetchMediaUriFor(Contribution contribution);
} }
} }

View file

@ -90,6 +90,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
private void initAdapter() { private void initAdapter() {
adapter = new ContributionsListAdapter(callback); adapter = new ContributionsListAdapter(callback);
adapter.setHasStableIds(true);
} }
@Override @Override

View file

@ -74,4 +74,8 @@ class ContributionsLocalDataSource {
public void set(String key, long value) { public void set(String key, long value) {
defaultKVStore.putLong(key,value); defaultKVStore.putLong(key,value);
} }
public Single<Integer> updateContribution(Contribution contribution) {
return contributionDao.update(contribution);
}
} }

View file

@ -12,6 +12,7 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import fr.free.nrw.commons.MediaDataExtractor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -62,6 +63,10 @@ public class ContributionsPresenter implements UserActionListener {
@Inject @Inject
SessionManager sessionManager; SessionManager sessionManager;
@Inject
MediaDataExtractor mediaDataExtractor;
private LifecycleOwner lifeCycleOwner; private LifecycleOwner lifeCycleOwner;
@Inject @Inject
@ -180,4 +185,24 @@ public class ContributionsPresenter implements UserActionListener {
} }
return contributionList.get(i); return contributionList.get(i);
} }
@Override
public void updateContribution(Contribution contribution) {
compositeDisposable.add(repository
.updateContribution(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
@Override
public void fetchMediaDetails(Contribution contribution) {
compositeDisposable.add(mediaDataExtractor
.getMediaFromFileName(contribution.filename)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(media -> {
contribution.thumbUrl=media.thumbUrl;
updateContribution(contribution);
}));
}
} }

View file

@ -61,4 +61,8 @@ public class ContributionsRepository {
public long getLong(String key) { public long getLong(String key) {
return localDataSource.getLong(key); return localDataSource.getLong(key);
} }
public Single<Integer> updateContribution(Contribution contribution) {
return localDataSource.updateContribution(contribution);
}
} }

View file

@ -1,36 +0,0 @@
package fr.free.nrw.commons.contributions.model;
import fr.free.nrw.commons.contributions.Contribution;
public class DisplayableContribution extends Contribution {
private int position;
public DisplayableContribution(Contribution contribution,
int position) {
super(contribution.getContentUri(),
contribution.getFilename(),
contribution.getLocalUri(),
contribution.getImageUrl(),
contribution.getDateCreated(),
contribution.getState(),
contribution.getDataLength(),
contribution.getDateUploaded(),
contribution.getTransferred(),
contribution.getSource(),
contribution.getDescription(),
contribution.getCreator(),
contribution.getMultiple(),
contribution.getWidth(),
contribution.getHeight(),
contribution.getLicense());
this._id=contribution._id;
this.position = position;
}
public int getPosition() {
return position;
}
public void setPosition(int position) {
this.position = position;
}
}

View file

@ -53,8 +53,6 @@ public interface CommonsApplicationComponent extends AndroidInjector<Application
void inject(PicOfDayAppWidget picOfDayAppWidget); void inject(PicOfDayAppWidget picOfDayAppWidget);
void inject(ContributionViewHolder viewHolder);
Gson gson(); Gson gson();
@Component.Builder @Component.Builder