mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 04:13:53 +01:00
Convert media package to kotlin (#6369)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
* Convert Caption to kotlin * Convert CaptionListViewAdapter to kotlin * Convert CaptionListViewAdapter to kotlin * Removed unused class * Converted MwParseResult / MwParseResponse to kotlin * Convert CustomOkHttpNetworkFetcher to kotlin * Break up MediaDetailPagerFragment to make it easier to convert to kotlin * Convert MediaDetailProvider to kotlin * Convert the MediaDetailAdapter to kotlin * Convert MediaDetailPagerFragment to kotlin
This commit is contained in:
parent
79f52db929
commit
8fc7e1039b
33 changed files with 1030 additions and 1184 deletions
|
|
@ -22,6 +22,7 @@ import fr.free.nrw.commons.contributions.MainActivity;
|
|||
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailProvider;
|
||||
import fr.free.nrw.commons.navtab.NavTab;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
|
|
@ -29,7 +30,7 @@ import timber.log.Timber;
|
|||
|
||||
public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements
|
||||
FragmentManager.OnBackStackChangedListener,
|
||||
MediaDetailPagerFragment.MediaDetailProvider,
|
||||
MediaDetailProvider,
|
||||
AdapterView.OnItemClickListener, CategoryImagesCallback {
|
||||
|
||||
private MediaDetailPagerFragment mediaDetails;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import android.view.Menu
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -22,6 +21,7 @@ import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment
|
|||
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment
|
||||
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment
|
||||
import fr.free.nrw.commons.media.MediaDetailProvider
|
||||
import fr.free.nrw.commons.theme.BaseActivity
|
||||
import fr.free.nrw.commons.utils.handleWebUrl
|
||||
import fr.free.nrw.commons.wikidata.model.WikiSite
|
||||
|
|
@ -36,7 +36,7 @@ import javax.inject.Inject
|
|||
* a particular category on wikimedia commons.
|
||||
*/
|
||||
class CategoryDetailsActivity : BaseActivity(),
|
||||
MediaDetailPagerFragment.MediaDetailProvider,
|
||||
MediaDetailProvider,
|
||||
CategoryImagesCallback {
|
||||
|
||||
private lateinit var supportFragmentManager: FragmentManager
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ 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.media.MediaDetailProvider
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||
import fr.free.nrw.commons.nearby.NearbyController
|
||||
import fr.free.nrw.commons.nearby.NearbyNotificationCardView
|
||||
|
|
@ -72,7 +72,6 @@ import io.reactivex.disposables.CompositeDisposable
|
|||
import io.reactivex.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import android.os.Bundle;
|
|||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
|
@ -17,10 +16,11 @@ import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding;
|
|||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailProvider;
|
||||
import fr.free.nrw.commons.navtab.NavTab;
|
||||
|
||||
public class ExploreListRootFragment extends CommonsDaggerSupportFragment implements
|
||||
MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
|
||||
MediaDetailProvider, CategoryImagesCallback {
|
||||
|
||||
private MediaDetailPagerFragment mediaDetails;
|
||||
private CategoriesMediaFragment listFragment;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import android.os.Bundle;
|
|||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
|
@ -17,10 +16,11 @@ import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding;
|
|||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.explore.map.ExploreMapFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailProvider;
|
||||
import fr.free.nrw.commons.navtab.NavTab;
|
||||
|
||||
public class ExploreMapRootFragment extends CommonsDaggerSupportFragment implements
|
||||
MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
|
||||
MediaDetailProvider, CategoryImagesCallback {
|
||||
|
||||
private MediaDetailPagerFragment mediaDetails;
|
||||
private ExploreMapFragment mapFragment;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import android.os.Bundle;
|
|||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
|
|
@ -23,18 +22,14 @@ import fr.free.nrw.commons.explore.models.RecentSearch;
|
|||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailProvider;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.utils.FragmentUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.inject.Inject;
|
||||
import kotlin.Pair;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
|
|
@ -42,7 +37,7 @@ import timber.log.Timber;
|
|||
*/
|
||||
|
||||
public class SearchActivity extends BaseActivity
|
||||
implements MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
|
||||
implements MediaDetailProvider, CategoryImagesCallback {
|
||||
|
||||
@Inject
|
||||
RecentSearchesDao recentSearchesDao;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import android.view.Menu;
|
|||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import fr.free.nrw.commons.Media;
|
||||
|
|
@ -24,6 +23,7 @@ import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment;
|
|||
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment;
|
||||
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailProvider;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictModel;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
|
|
@ -31,16 +31,12 @@ import fr.free.nrw.commons.wikidata.WikidataConstants;
|
|||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import kotlin.Pair;
|
||||
|
||||
/**
|
||||
* Activity to show depiction media, parent classes and child classes of depicted items in Explore
|
||||
*/
|
||||
public class WikidataItemDetailsActivity extends BaseActivity implements MediaDetailPagerFragment.MediaDetailProvider,
|
||||
public class WikidataItemDetailsActivity extends BaseActivity implements MediaDetailProvider,
|
||||
CategoryImagesCallback {
|
||||
private FragmentManager supportFragmentManager;
|
||||
private DepictedImagesFragment depictionImagesListFragment;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import fr.free.nrw.commons.MediaDataExtractor
|
|||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.category.CategoryImagesCallback
|
||||
import fr.free.nrw.commons.explore.paging.BasePagingFragment
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider
|
||||
import fr.free.nrw.commons.media.MediaDetailProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class PageableMediaFragment :
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Model class for parsing Captions when fetching captions using filename in MediaClient
|
||||
*/
|
||||
public class Caption {
|
||||
|
||||
/**
|
||||
* users language in which caption is written
|
||||
*/
|
||||
@SerializedName("language")
|
||||
private String language;
|
||||
@SerializedName("value")
|
||||
private String value;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*/
|
||||
public Caption() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value
|
||||
* @param language
|
||||
*/
|
||||
public Caption(String language, String value) {
|
||||
super();
|
||||
this.language = language;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@SerializedName("language")
|
||||
public String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
@SerializedName("value")
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
19
app/src/main/java/fr/free/nrw/commons/media/Caption.kt
Normal file
19
app/src/main/java/fr/free/nrw/commons/media/Caption.kt
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* Model class for parsing Captions when fetching captions using filename in MediaClient
|
||||
*/
|
||||
class Caption() {
|
||||
@SerializedName("language")
|
||||
var language: String? = null
|
||||
|
||||
@SerializedName("value")
|
||||
var value: String? = null
|
||||
|
||||
constructor(language: String?, value: String?) : this() {
|
||||
this.language = language
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.TextView;
|
||||
import fr.free.nrw.commons.R;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Adapter for Caption Listview
|
||||
*/
|
||||
public class CaptionListViewAdapter extends BaseAdapter {
|
||||
|
||||
List<Caption> captions;
|
||||
|
||||
public CaptionListViewAdapter(final List<Caption> captions) {
|
||||
this.captions = captions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return size of captions list
|
||||
*/
|
||||
@Override
|
||||
public int getCount() {
|
||||
return captions.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Object at position i
|
||||
*/
|
||||
@Override
|
||||
public Object getItem(final int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return id for current item
|
||||
*/
|
||||
@Override
|
||||
public long getItemId(final int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* inflate the view and bind data with UI
|
||||
*/
|
||||
@Override
|
||||
public View getView(final int i, final View view, final ViewGroup viewGroup) {
|
||||
final TextView captionLanguageTextView;
|
||||
final TextView captionTextView;
|
||||
final View captionLayout = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.caption_item, null);
|
||||
captionLanguageTextView = captionLayout.findViewById(R.id.caption_language_textview);
|
||||
captionTextView = captionLayout.findViewById(R.id.caption_text);
|
||||
if (captions.size() == 1 && captions.get(0).getValue().equals("No Caption")) {
|
||||
captionLanguageTextView.setText(captions.get(i).getLanguage());
|
||||
captionTextView.setText(captions.get(i).getValue());
|
||||
} else {
|
||||
captionLanguageTextView.setText(captions.get(i).getLanguage() + ":");
|
||||
captionTextView.setText(captions.get(i).getValue());
|
||||
}
|
||||
|
||||
return captionLayout;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.TextView
|
||||
import fr.free.nrw.commons.R
|
||||
|
||||
/**
|
||||
* Adapter for Caption Listview
|
||||
*/
|
||||
class CaptionListViewAdapter(var captions: List<Caption>) : BaseAdapter() {
|
||||
override fun getCount(): Int = captions.size
|
||||
|
||||
override fun getItem(i: Int): Any? = null
|
||||
|
||||
override fun getItemId(i: Int): Long = 0
|
||||
|
||||
override fun getView(i: Int, view: View, viewGroup: ViewGroup): View {
|
||||
val captionLayout = LayoutInflater.from(viewGroup.context).inflate(R.layout.caption_item, null)
|
||||
val captionLanguageTextView = captionLayout.findViewById<TextView>(R.id.caption_language_textview)
|
||||
val captionTextView = captionLayout.findViewById<TextView>(R.id.caption_text)
|
||||
if (captions.size == 1 && captions[0].value == "No Caption") {
|
||||
captionLanguageTextView.text = captions[i].language
|
||||
captionTextView.text = captions[i].value
|
||||
} else {
|
||||
captionLanguageTextView.text = captions[i].language + ":"
|
||||
captionTextView.text = captions[i].value
|
||||
}
|
||||
|
||||
return captionLayout
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* Represents the Wikibase item associated with a Wikimedia Commons file.
|
||||
* For instance the Wikibase item M63996 represents the Commons file "Paul Cézanne - The Pigeon Tower at Bellevue - 1936.19 - Cleveland Museum of Art.jpg"
|
||||
*/
|
||||
public class CommonsWikibaseItem {
|
||||
|
||||
@SerializedName("type")
|
||||
private String type;
|
||||
@SerializedName("id")
|
||||
private String id;
|
||||
@SerializedName("labels")
|
||||
private Map<String, Caption> labels;
|
||||
@SerializedName("statements")
|
||||
private Object statements = null;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*/
|
||||
public CommonsWikibaseItem() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id
|
||||
* @param statements
|
||||
* @param labels
|
||||
* @param type
|
||||
*/
|
||||
public CommonsWikibaseItem(String type, String id, Map<String, Caption> labels, Object statements) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
this.labels = labels;
|
||||
this.statements = statements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ex: "mediainfo
|
||||
*/
|
||||
@SerializedName("type")
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Wikibase Id
|
||||
*/
|
||||
@SerializedName("id")
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return value of captions
|
||||
*/
|
||||
@SerializedName("labels")
|
||||
public Map<String, Caption> getLabels() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains the Depicts item
|
||||
*/
|
||||
@SerializedName("statements")
|
||||
public Object getStatements() {
|
||||
return statements;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.facebook.imagepipeline.common.BytesRange;
|
||||
import com.facebook.imagepipeline.image.EncodedImage;
|
||||
import com.facebook.imagepipeline.producers.BaseNetworkFetcher;
|
||||
import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks;
|
||||
import com.facebook.imagepipeline.producers.Consumer;
|
||||
import com.facebook.imagepipeline.producers.FetchState;
|
||||
import com.facebook.imagepipeline.producers.NetworkFetcher;
|
||||
import com.facebook.imagepipeline.producers.ProducerContext;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import okhttp3.CacheControl;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import timber.log.Timber;
|
||||
|
||||
// Custom implementation of Fresco's Network fetcher to skip downloading of images when limited connection mode is enabled
|
||||
// https://github.com/facebook/fresco/blob/master/imagepipeline-backends/imagepipeline-okhttp3/src/main/java/com/facebook/imagepipeline/backends/okhttp3/OkHttpNetworkFetcher.java
|
||||
@Singleton
|
||||
public class CustomOkHttpNetworkFetcher
|
||||
extends BaseNetworkFetcher<CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState> {
|
||||
|
||||
private static final String QUEUE_TIME = "queue_time";
|
||||
private static final String FETCH_TIME = "fetch_time";
|
||||
private static final String TOTAL_TIME = "total_time";
|
||||
private static final String IMAGE_SIZE = "image_size";
|
||||
private final Call.Factory mCallFactory;
|
||||
private final @Nullable
|
||||
CacheControl mCacheControl;
|
||||
private final Executor mCancellationExecutor;
|
||||
private final JsonKvStore defaultKvStore;
|
||||
|
||||
/**
|
||||
* @param okHttpClient client to use
|
||||
*/
|
||||
@Inject
|
||||
public CustomOkHttpNetworkFetcher(final OkHttpClient okHttpClient,
|
||||
@Named("default_preferences") final JsonKvStore defaultKvStore) {
|
||||
this(okHttpClient, okHttpClient.dispatcher().executorService(), defaultKvStore);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callFactory custom {@link Call.Factory} for fetching image from the network
|
||||
* @param cancellationExecutor executor on which fetching cancellation is performed if
|
||||
* cancellation is requested from the UI Thread
|
||||
*/
|
||||
public CustomOkHttpNetworkFetcher(final Call.Factory callFactory,
|
||||
final Executor cancellationExecutor,
|
||||
final JsonKvStore defaultKvStore) {
|
||||
this(callFactory, cancellationExecutor, defaultKvStore, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callFactory custom {@link Call.Factory} for fetching image from the network
|
||||
* @param cancellationExecutor executor on which fetching cancellation is performed if
|
||||
* cancellation is requested from the UI Thread
|
||||
* @param disableOkHttpCache true if network requests should not be cached by OkHttp
|
||||
*/
|
||||
public CustomOkHttpNetworkFetcher(
|
||||
final Call.Factory callFactory, final Executor cancellationExecutor,
|
||||
final JsonKvStore defaultKvStore,
|
||||
final boolean disableOkHttpCache) {
|
||||
this.defaultKvStore = defaultKvStore;
|
||||
mCallFactory = callFactory;
|
||||
mCancellationExecutor = cancellationExecutor;
|
||||
mCacheControl = disableOkHttpCache ? new CacheControl.Builder().noStore().build() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OkHttpNetworkFetchState createFetchState(
|
||||
final Consumer<EncodedImage> consumer, final ProducerContext context) {
|
||||
return new OkHttpNetworkFetchState(consumer, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fetch(
|
||||
final OkHttpNetworkFetchState fetchState, final NetworkFetcher.Callback callback) {
|
||||
fetchState.submitTime = SystemClock.elapsedRealtime();
|
||||
final Uri uri = fetchState.getUri();
|
||||
|
||||
try {
|
||||
if (defaultKvStore
|
||||
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
|
||||
Timber.d("Skipping loading of image as limited connection mode is enabled");
|
||||
callback.onFailure(
|
||||
new Exception("Failing image request as limited connection mode is enabled"));
|
||||
return;
|
||||
}
|
||||
final Request.Builder requestBuilder = new Request.Builder().url(uri.toString()).get();
|
||||
|
||||
if (mCacheControl != null) {
|
||||
requestBuilder.cacheControl(mCacheControl);
|
||||
}
|
||||
|
||||
final BytesRange bytesRange = fetchState.getContext().getImageRequest().getBytesRange();
|
||||
if (bytesRange != null) {
|
||||
requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue());
|
||||
}
|
||||
|
||||
fetchWithRequest(fetchState, callback, requestBuilder.build());
|
||||
} catch (final Exception e) {
|
||||
// handle error while creating the request
|
||||
callback.onFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchCompletion(final OkHttpNetworkFetchState fetchState, final int byteSize) {
|
||||
fetchState.fetchCompleteTime = SystemClock.elapsedRealtime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getExtraMap(final OkHttpNetworkFetchState fetchState,
|
||||
final int byteSize) {
|
||||
final Map<String, String> extraMap = new HashMap<>(4);
|
||||
extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime));
|
||||
extraMap
|
||||
.put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime));
|
||||
extraMap
|
||||
.put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime));
|
||||
extraMap.put(IMAGE_SIZE, Integer.toString(byteSize));
|
||||
return extraMap;
|
||||
}
|
||||
|
||||
protected void fetchWithRequest(
|
||||
final OkHttpNetworkFetchState fetchState,
|
||||
final NetworkFetcher.Callback callback,
|
||||
final Request request) {
|
||||
final Call call = mCallFactory.newCall(request);
|
||||
|
||||
fetchState
|
||||
.getContext()
|
||||
.addCallbacks(
|
||||
new BaseProducerContextCallbacks() {
|
||||
@Override
|
||||
public void onCancellationRequested() {
|
||||
onFetchCancellationRequested(call);
|
||||
}
|
||||
});
|
||||
|
||||
call.enqueue(
|
||||
new okhttp3.Callback() {
|
||||
@Override
|
||||
public void onResponse(final Call call, final Response response) {
|
||||
onFetchResponse(fetchState, call, response, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final Call call, final IOException e) {
|
||||
handleException(call, e, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onFetchCancellationRequested(final Call call) {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
call.cancel();
|
||||
} else {
|
||||
mCancellationExecutor.execute(call::cancel);
|
||||
}
|
||||
}
|
||||
|
||||
private void onFetchResponse(final OkHttpNetworkFetchState fetchState, final Call call,
|
||||
final Response response,
|
||||
final NetworkFetcher.Callback callback) {
|
||||
fetchState.responseTime = SystemClock.elapsedRealtime();
|
||||
try (final ResponseBody body = response.body()) {
|
||||
if (!response.isSuccessful()) {
|
||||
handleException(
|
||||
call, new IOException("Unexpected HTTP code " + response),
|
||||
callback);
|
||||
return;
|
||||
}
|
||||
|
||||
final BytesRange responseRange =
|
||||
BytesRange.fromContentRangeHeader(response.header("Content-Range"));
|
||||
if (responseRange != null
|
||||
&& !(responseRange.from == 0
|
||||
&& responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
|
||||
// Only treat as a partial image if the range is not all of the content
|
||||
fetchState.setResponseBytesRange(responseRange);
|
||||
fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT);
|
||||
}
|
||||
|
||||
long contentLength = body.contentLength();
|
||||
if (contentLength < 0) {
|
||||
contentLength = 0;
|
||||
}
|
||||
callback.onResponse(body.byteStream(), (int) contentLength);
|
||||
} catch (final Exception e) {
|
||||
handleException(call, e, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles exceptions.
|
||||
*
|
||||
* <p>OkHttp notifies callers of cancellations via an IOException. If IOException is caught
|
||||
* after request cancellation, then the exception is interpreted as successful cancellation and
|
||||
* onCancellation is called. Otherwise onFailure is called.
|
||||
*/
|
||||
private void handleException(final Call call, final Exception e, final Callback callback) {
|
||||
if (call.isCanceled()) {
|
||||
callback.onCancellation();
|
||||
} else {
|
||||
callback.onFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class OkHttpNetworkFetchState extends FetchState {
|
||||
|
||||
public long submitTime;
|
||||
public long responseTime;
|
||||
public long fetchCompleteTime;
|
||||
|
||||
public OkHttpNetworkFetchState(
|
||||
final Consumer<EncodedImage> consumer, final ProducerContext producerContext) {
|
||||
super(consumer, producerContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import com.facebook.imagepipeline.common.BytesRange
|
||||
import com.facebook.imagepipeline.image.EncodedImage
|
||||
import com.facebook.imagepipeline.producers.BaseNetworkFetcher
|
||||
import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks
|
||||
import com.facebook.imagepipeline.producers.Consumer
|
||||
import com.facebook.imagepipeline.producers.FetchState
|
||||
import com.facebook.imagepipeline.producers.NetworkFetcher
|
||||
import com.facebook.imagepipeline.producers.ProducerContext
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executor
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
// Custom implementation of Fresco's Network fetcher to skip downloading of images when limited connection mode is enabled
|
||||
// https://github.com/facebook/fresco/blob/master/imagepipeline-backends/imagepipeline-okhttp3/src/main/java/com/facebook/imagepipeline/backends/okhttp3/OkHttpNetworkFetcher.java
|
||||
@Singleton
|
||||
class CustomOkHttpNetworkFetcher
|
||||
@JvmOverloads constructor(
|
||||
private val mCallFactory: Call.Factory,
|
||||
private val mCancellationExecutor: Executor,
|
||||
private val defaultKvStore: JsonKvStore,
|
||||
disableOkHttpCache: Boolean = true
|
||||
) : BaseNetworkFetcher<OkHttpNetworkFetchState>() {
|
||||
|
||||
private val mCacheControl =
|
||||
if (disableOkHttpCache) CacheControl.Builder().noStore().build() else null
|
||||
private val isLimitedConnectionMode: Boolean
|
||||
get() = defaultKvStore.getBoolean(
|
||||
CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
|
||||
false
|
||||
)
|
||||
|
||||
/**
|
||||
* @param okHttpClient client to use
|
||||
*/
|
||||
@Inject
|
||||
constructor(
|
||||
okHttpClient: OkHttpClient,
|
||||
@Named("default_preferences") defaultKvStore: JsonKvStore
|
||||
) : this(okHttpClient, okHttpClient.dispatcher.executorService, defaultKvStore)
|
||||
|
||||
/**
|
||||
* @param mCallFactory custom [Call.Factory] for fetching image from the network
|
||||
* @param mCancellationExecutor executor on which fetching cancellation is performed if
|
||||
* cancellation is requested from the UI Thread
|
||||
* @param disableOkHttpCache true if network requests should not be cached by OkHttp
|
||||
*/
|
||||
override fun createFetchState(consumer: Consumer<EncodedImage>, context: ProducerContext) =
|
||||
OkHttpNetworkFetchState(consumer, context)
|
||||
|
||||
override fun fetch(
|
||||
fetchState: OkHttpNetworkFetchState, callback: NetworkFetcher.Callback
|
||||
) {
|
||||
fetchState.submitTime = SystemClock.elapsedRealtime()
|
||||
|
||||
try {
|
||||
if (isLimitedConnectionMode) {
|
||||
Timber.d("Skipping loading of image as limited connection mode is enabled")
|
||||
callback.onFailure(Exception("Failing image request as limited connection mode is enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
val requestBuilder = Request.Builder().url(fetchState.uri.toString()).get()
|
||||
|
||||
if (mCacheControl != null) {
|
||||
requestBuilder.cacheControl(mCacheControl)
|
||||
}
|
||||
|
||||
val bytesRange = fetchState.context.imageRequest.bytesRange
|
||||
if (bytesRange != null) {
|
||||
requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue())
|
||||
}
|
||||
|
||||
fetchWithRequest(fetchState, callback, requestBuilder.build())
|
||||
} catch (e: Exception) {
|
||||
// handle error while creating the request
|
||||
callback.onFailure(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFetchCompletion(fetchState: OkHttpNetworkFetchState, byteSize: Int) {
|
||||
fetchState.fetchCompleteTime = SystemClock.elapsedRealtime()
|
||||
}
|
||||
|
||||
override fun getExtraMap(fetchState: OkHttpNetworkFetchState, byteSize: Int) =
|
||||
fetchState.toExtraMap(byteSize)
|
||||
|
||||
private fun fetchWithRequest(
|
||||
fetchState: OkHttpNetworkFetchState, callback: NetworkFetcher.Callback, request: Request
|
||||
) {
|
||||
val call = mCallFactory.newCall(request)
|
||||
|
||||
fetchState.context.addCallbacks(object : BaseProducerContextCallbacks() {
|
||||
override fun onCancellationRequested() {
|
||||
onFetchCancellationRequested(call)
|
||||
}
|
||||
})
|
||||
|
||||
call.enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) =
|
||||
onFetchResponse(fetchState, call, response, callback)
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) =
|
||||
handleException(call, e, callback)
|
||||
})
|
||||
}
|
||||
|
||||
private fun onFetchCancellationRequested(call: Call) {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
call.cancel()
|
||||
} else {
|
||||
mCancellationExecutor.execute { call.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFetchResponse(
|
||||
fetchState: OkHttpNetworkFetchState,
|
||||
call: Call,
|
||||
response: Response,
|
||||
callback: NetworkFetcher.Callback
|
||||
) {
|
||||
fetchState.responseTime = SystemClock.elapsedRealtime()
|
||||
try {
|
||||
response.body.use { body ->
|
||||
if (!response.isSuccessful) {
|
||||
handleException(call, IOException("Unexpected HTTP code $response"), callback)
|
||||
return
|
||||
}
|
||||
val responseRange =
|
||||
BytesRange.fromContentRangeHeader(response.header("Content-Range"))
|
||||
if (responseRange != null && !(responseRange.from == 0 && responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
|
||||
// Only treat as a partial image if the range is not all of the content
|
||||
fetchState.responseBytesRange = responseRange
|
||||
fetchState.onNewResultStatusFlags = Consumer.IS_PARTIAL_RESULT
|
||||
}
|
||||
|
||||
var contentLength = body!!.contentLength()
|
||||
if (contentLength < 0) {
|
||||
contentLength = 0
|
||||
}
|
||||
callback.onResponse(body.byteStream(), contentLength.toInt())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
handleException(call, e, callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles exceptions.
|
||||
*
|
||||
* OkHttp notifies callers of cancellations via an IOException. If IOException is caught
|
||||
* after request cancellation, then the exception is interpreted as successful cancellation and
|
||||
* onCancellation is called. Otherwise onFailure is called.
|
||||
*/
|
||||
private fun handleException(call: Call, e: Exception, callback: NetworkFetcher.Callback) {
|
||||
if (call.isCanceled()) {
|
||||
callback.onCancellation()
|
||||
} else {
|
||||
callback.onFailure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OkHttpNetworkFetchState(
|
||||
consumer: Consumer<EncodedImage>?, producerContext: ProducerContext?
|
||||
) : FetchState(consumer, producerContext) {
|
||||
var submitTime: Long = 0
|
||||
var responseTime: Long = 0
|
||||
var fetchCompleteTime: Long = 0
|
||||
|
||||
fun toExtraMap(byteSize: Int) = buildMap {
|
||||
put(QUEUE_TIME, (responseTime - submitTime).toString())
|
||||
put(FETCH_TIME, (fetchCompleteTime - responseTime).toString())
|
||||
put(TOTAL_TIME, (fetchCompleteTime - submitTime).toString())
|
||||
put(IMAGE_SIZE, byteSize.toString())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val QUEUE_TIME = "queue_time"
|
||||
private const val FETCH_TIME = "fetch_time"
|
||||
private const val TOTAL_TIME = "total_time"
|
||||
private const val IMAGE_SIZE = "image_size"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter
|
||||
import fr.free.nrw.commons.media.MediaDetailFragment.Companion.forMedia
|
||||
import timber.log.Timber
|
||||
|
||||
// FragmentStatePagerAdapter allows user to swipe across collection of images (no. of images undetermined)
|
||||
class MediaDetailAdapter(
|
||||
val mediaDetailPagerFragment: MediaDetailPagerFragment,
|
||||
fm: FragmentManager
|
||||
) : FragmentStatePagerAdapter(fm) {
|
||||
/**
|
||||
* Keeps track of the current displayed fragment.
|
||||
*/
|
||||
private var currentFragment: Fragment? = null
|
||||
|
||||
override fun getItem(i: Int): Fragment {
|
||||
if (i == 0) {
|
||||
// See bug https://code.google.com/p/android/issues/detail?id=27526
|
||||
if (mediaDetailPagerFragment.activity == null) {
|
||||
Timber.d("Skipping getItem. Returning as activity is destroyed!")
|
||||
return Fragment()
|
||||
}
|
||||
mediaDetailPagerFragment.binding!!.mediaDetailsPager.postDelayed(
|
||||
{ mediaDetailPagerFragment.requireActivity().invalidateOptionsMenu() }, 5
|
||||
)
|
||||
}
|
||||
return if (mediaDetailPagerFragment.isFromFeaturedRootFragment) {
|
||||
forMedia(
|
||||
mediaDetailPagerFragment.position + i,
|
||||
mediaDetailPagerFragment.editable, mediaDetailPagerFragment.isFeaturedImage,
|
||||
mediaDetailPagerFragment.isWikipediaButtonDisplayed
|
||||
)
|
||||
} else {
|
||||
forMedia(
|
||||
i, mediaDetailPagerFragment.editable,
|
||||
mediaDetailPagerFragment.isFeaturedImage,
|
||||
mediaDetailPagerFragment.isWikipediaButtonDisplayed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
if (mediaDetailPagerFragment.activity == null) {
|
||||
Timber.d("Skipping getCount. Returning as activity is destroyed!")
|
||||
return 0
|
||||
}
|
||||
return mediaDetailPagerFragment.mediaDetailProvider!!.getTotalMediaCount()
|
||||
}
|
||||
|
||||
/**
|
||||
* If current fragment is of type MediaDetailFragment, return it, otherwise return null.
|
||||
*
|
||||
* @return MediaDetailFragment
|
||||
*/
|
||||
val currentMediaDetailFragment: MediaDetailFragment?
|
||||
get() = currentFragment as? MediaDetailFragment
|
||||
|
||||
/**
|
||||
* Called to inform the adapter of which item is currently considered to be the "primary", that
|
||||
* is the one show to the user as the current page.
|
||||
*/
|
||||
override fun setPrimaryItem(
|
||||
container: ViewGroup, position: Int,
|
||||
obj: Any
|
||||
) {
|
||||
// Update the current fragment if changed
|
||||
if (currentFragment !== obj) {
|
||||
currentFragment = (obj as Fragment)
|
||||
}
|
||||
super.setPrimaryItem(container, position, obj)
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +77,6 @@ import fr.free.nrw.commons.CommonsApplication.Companion.instance
|
|||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.MediaDataExtractor
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.utils.UnderlineUtils
|
||||
import fr.free.nrw.commons.actions.ThanksClient
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||
|
|
@ -102,7 +101,6 @@ import fr.free.nrw.commons.kvstore.JsonKvStore
|
|||
import fr.free.nrw.commons.language.AppLanguageLookUpTable
|
||||
import fr.free.nrw.commons.location.LocationServiceManager
|
||||
import fr.free.nrw.commons.locationpicker.LocationPicker
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider
|
||||
import fr.free.nrw.commons.profile.ProfileActivity
|
||||
import fr.free.nrw.commons.review.ReviewHelper
|
||||
import fr.free.nrw.commons.settings.Prefs
|
||||
|
|
|
|||
|
|
@ -1,678 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import static fr.free.nrw.commons.utils.UrlUtilsKt.handleWebUrl;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.widget.ProgressBar;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.utils.ClipboardUtils;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.bookmarks.models.Bookmark;
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.databinding.FragmentMediaDetailPagerBinding;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
import fr.free.nrw.commons.profile.ProfileActivity;
|
||||
import fr.free.nrw.commons.utils.DownloadUtils;
|
||||
import fr.free.nrw.commons.utils.ImageUtils;
|
||||
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.disposables.CompositeDisposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
import javax.inject.Inject;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener, MediaDetailFragment.Callback {
|
||||
|
||||
@Inject BookmarkPicturesDao bookmarkDao;
|
||||
|
||||
@Inject
|
||||
protected OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
|
||||
@Inject
|
||||
protected SessionManager sessionManager;
|
||||
|
||||
private static CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
private FragmentMediaDetailPagerBinding binding;
|
||||
|
||||
private boolean editable;
|
||||
private boolean isFeaturedImage;
|
||||
private boolean isWikipediaButtonDisplayed;
|
||||
MediaDetailAdapter adapter;
|
||||
private Bookmark bookmark;
|
||||
private MediaDetailProvider provider;
|
||||
private boolean isFromFeaturedRootFragment;
|
||||
private int position;
|
||||
|
||||
/**
|
||||
* ProgressBar used to indicate the loading status of media items.
|
||||
*/
|
||||
private ProgressBar imageProgressBar;
|
||||
|
||||
private ArrayList<Integer> removedItems=new ArrayList<Integer>();
|
||||
|
||||
public void clearRemoved(){
|
||||
removedItems.clear();
|
||||
}
|
||||
public ArrayList<Integer> getRemovedItems() {
|
||||
return removedItems;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Use this factory method to create a new instance of this fragment using the provided
|
||||
* parameters.
|
||||
*
|
||||
* This method will create a new instance of MediaDetailPagerFragment and the arguments will be
|
||||
* saved to a bundle which will be later available in the {@link #onCreate(Bundle)}
|
||||
* @param editable
|
||||
* @param isFeaturedImage
|
||||
* @return
|
||||
*/
|
||||
public static MediaDetailPagerFragment newInstance(boolean editable, boolean isFeaturedImage) {
|
||||
MediaDetailPagerFragment mediaDetailPagerFragment = new MediaDetailPagerFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putBoolean("is_editable", editable);
|
||||
args.putBoolean("is_featured_image", isFeaturedImage);
|
||||
mediaDetailPagerFragment.setArguments(args);
|
||||
return mediaDetailPagerFragment;
|
||||
}
|
||||
|
||||
public MediaDetailPagerFragment() {
|
||||
// Required empty public constructor
|
||||
};
|
||||
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
binding = FragmentMediaDetailPagerBinding.inflate(inflater, container, false);
|
||||
binding.mediaDetailsPager.addOnPageChangeListener(this);
|
||||
// Initialize the ProgressBar by finding it in the layout
|
||||
imageProgressBar = binding.getRoot().findViewById(R.id.itemProgressBar);
|
||||
adapter = new MediaDetailAdapter(getChildFragmentManager());
|
||||
|
||||
// ActionBar is now supported in both activities - if this crashes something is quite wrong
|
||||
final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
else {
|
||||
throw new AssertionError("Action bar should not be null!");
|
||||
}
|
||||
|
||||
// If fragment is associated with ProfileActivity, then hide the tabLayout
|
||||
if (getActivity() instanceof ProfileActivity) {
|
||||
((ProfileActivity)getActivity()).setTabLayoutVisibility(false);
|
||||
}
|
||||
|
||||
// Else if fragment is associated with MainActivity then hide that tab layout
|
||||
else if (getActivity() instanceof MainActivity) {
|
||||
((MainActivity)getActivity()).hideTabs();
|
||||
}
|
||||
|
||||
binding.mediaDetailsPager.setAdapter(adapter);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
final int pageNumber = savedInstanceState.getInt("current-page");
|
||||
binding.mediaDetailsPager.setCurrentItem(pageNumber, false);
|
||||
getActivity().invalidateOptionsMenu();
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt("current-page", binding.mediaDetailsPager.getCurrentItem());
|
||||
outState.putBoolean("editable", editable);
|
||||
outState.putBoolean("isFeaturedImage", isFeaturedImage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
editable = savedInstanceState.getBoolean("editable", false);
|
||||
isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage", false);
|
||||
|
||||
}
|
||||
setHasOptionsMenu(true);
|
||||
initProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* initialise the provider, based on from where the fragment was started, as in from an activity
|
||||
* or a fragment
|
||||
*/
|
||||
private void initProvider() {
|
||||
if (getParentFragment() instanceof MediaDetailProvider) {
|
||||
provider = (MediaDetailProvider) getParentFragment();
|
||||
} else if (getActivity() instanceof MediaDetailProvider) {
|
||||
provider = (MediaDetailProvider) getActivity();
|
||||
} else {
|
||||
throw new ClassCastException("Parent must implement MediaDetailProvider");
|
||||
}
|
||||
}
|
||||
|
||||
public MediaDetailProvider getMediaDetailProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (getActivity() == null) {
|
||||
Timber.d("Returning as activity is destroyed!");
|
||||
return true;
|
||||
}
|
||||
|
||||
Media m = provider.getMediaAtPosition(binding.mediaDetailsPager.getCurrentItem());
|
||||
MediaDetailFragment mediaDetailFragment = this.adapter.getCurrentMediaDetailFragment();
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_bookmark_current_image:
|
||||
boolean bookmarkExists = bookmarkDao.updateBookmark(bookmark);
|
||||
Snackbar snackbar = bookmarkExists ? Snackbar.make(getView(), R.string.add_bookmark, Snackbar.LENGTH_LONG) : Snackbar.make(getView(), R.string.remove_bookmark, Snackbar.LENGTH_LONG);
|
||||
snackbar.show();
|
||||
updateBookmarkState(item);
|
||||
return true;
|
||||
case R.id.menu_copy_link:
|
||||
String uri = m.getPageTitle().getCanonicalUri();
|
||||
ClipboardUtils.copy("shareLink", uri, requireContext());
|
||||
Timber.d("Copied share link to clipboard: %s", uri);
|
||||
Toast.makeText(requireContext(), getString(R.string.menu_link_copied),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
case R.id.menu_share_current_image:
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.setType("text/plain");
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, m.getDisplayTitle() + " \n" + m.getPageTitle().getCanonicalUri());
|
||||
startActivity(Intent.createChooser(shareIntent, "Share image via..."));
|
||||
|
||||
//Add media detail to backstack when the share button is clicked
|
||||
//So that when the share is cancelled or completed the media detail page is on top
|
||||
// of back stack fixing:https://github.com/commons-app/apps-android-commons/issues/2296
|
||||
FragmentManager supportFragmentManager = getActivity().getSupportFragmentManager();
|
||||
if (supportFragmentManager.getBackStackEntryCount() < 2) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.addToBackStack(MediaDetailPagerFragment.class.getName())
|
||||
.commit();
|
||||
supportFragmentManager.executePendingTransactions();
|
||||
}
|
||||
return true;
|
||||
case R.id.menu_browser_current_image:
|
||||
// View in browser
|
||||
handleWebUrl(requireContext(), Uri.parse(m.getPageTitle().getMobileUri()));
|
||||
return true;
|
||||
case R.id.menu_download_current_image:
|
||||
// Download
|
||||
if (!NetworkUtils.isInternetConnectionEstablished(getActivity())) {
|
||||
ViewUtil.showShortSnackbar(getView(), R.string.no_internet);
|
||||
return false;
|
||||
}
|
||||
DownloadUtils.downloadMedia(getActivity(), m);
|
||||
return true;
|
||||
case R.id.menu_set_as_wallpaper:
|
||||
// Set wallpaper
|
||||
setWallpaper(m);
|
||||
return true;
|
||||
case R.id.menu_set_as_avatar:
|
||||
// Set avatar
|
||||
setAvatar(m);
|
||||
return true;
|
||||
case R.id.menu_view_user_page:
|
||||
if (m != null && m.getUser() != null) {
|
||||
ProfileActivity.startYourself(getActivity(), m.getUser(),
|
||||
!Objects.equals(sessionManager.getUserName(), m.getUser()));
|
||||
}
|
||||
return true;
|
||||
case R.id.menu_view_report:
|
||||
showReportDialog(m);
|
||||
case R.id.menu_view_set_white_background:
|
||||
if (mediaDetailFragment != null) {
|
||||
mediaDetailFragment.onImageBackgroundChanged(ContextCompat.getColor(getContext(), R.color.white));
|
||||
}
|
||||
return true;
|
||||
case R.id.menu_view_set_black_background:
|
||||
if (mediaDetailFragment != null) {
|
||||
mediaDetailFragment.onImageBackgroundChanged(ContextCompat.getColor(getContext(), R.color.black));
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void showReportDialog(final Media media) {
|
||||
if (media == null) {
|
||||
return;
|
||||
}
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
|
||||
final String[] values = requireContext().getResources()
|
||||
.getStringArray(R.array.report_violation_options);
|
||||
builder.setTitle(R.string.report_violation);
|
||||
builder.setItems(R.array.report_violation_options, (dialog, which) -> {
|
||||
sendReportEmail(media, values[which]);
|
||||
});
|
||||
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {});
|
||||
builder.setCancelable(false);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void sendReportEmail(final Media media, final String type) {
|
||||
final String technicalInfo = getTechInfo(media, type);
|
||||
|
||||
final Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO);
|
||||
feedbackIntent.setType("message/rfc822");
|
||||
feedbackIntent.setData(Uri.parse("mailto:"));
|
||||
feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
|
||||
new String[]{CommonsApplication.REPORT_EMAIL});
|
||||
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
|
||||
CommonsApplication.REPORT_EMAIL_SUBJECT);
|
||||
feedbackIntent.putExtra(Intent.EXTRA_TEXT, technicalInfo);
|
||||
try {
|
||||
startActivity(feedbackIntent);
|
||||
} catch (final ActivityNotFoundException e) {
|
||||
Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private String getTechInfo(final Media media, final String type) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append("Report type: ")
|
||||
.append(type)
|
||||
.append("\n\n");
|
||||
|
||||
builder.append("Image that you want to report: ")
|
||||
.append(media.getImageUrl())
|
||||
.append("\n\n");
|
||||
|
||||
builder.append("User that you want to report: ")
|
||||
.append(media.getUser())
|
||||
.append("\n\n");
|
||||
|
||||
if (sessionManager.getUserName() != null) {
|
||||
builder.append("Your username: ")
|
||||
.append(sessionManager.getUserName())
|
||||
.append("\n\n");
|
||||
}
|
||||
|
||||
builder.append("Violation reason: ")
|
||||
.append("\n");
|
||||
|
||||
builder.append("----------------------------------------------")
|
||||
.append("\n")
|
||||
.append("(please write reason here)")
|
||||
.append("\n")
|
||||
.append("----------------------------------------------")
|
||||
.append("\n\n")
|
||||
.append("Thank you for your report! Our team will investigate as soon as possible.")
|
||||
.append("\n")
|
||||
.append("Please note that images also have a `Nominate for deletion` button.");
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the media as the device's wallpaper if the imageUrl is not null
|
||||
* Fails silently if setting the wallpaper fails
|
||||
* @param media
|
||||
*/
|
||||
private void setWallpaper(Media media) {
|
||||
if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
|
||||
Timber.d("Media URL not present");
|
||||
return;
|
||||
}
|
||||
ImageUtils.setWallpaperFromImageUrl(getActivity(), Uri.parse(media.getImageUrl()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the media as user's leaderboard avatar
|
||||
* @param media
|
||||
*/
|
||||
private void setAvatar(Media media) {
|
||||
if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
|
||||
Timber.d("Media URL not present");
|
||||
return;
|
||||
}
|
||||
ImageUtils.setAvatarFromImageUrl(getActivity(), media.getImageUrl(),
|
||||
Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
|
||||
okHttpJsonApiClient, compositeDisposable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
if (!editable) { // Disable menu options for editable views
|
||||
menu.clear(); // see http://stackoverflow.com/a/8495697/17865
|
||||
inflater.inflate(R.menu.fragment_image_detail, menu);
|
||||
if (binding.mediaDetailsPager != null) {
|
||||
MediaDetailProvider provider = getMediaDetailProvider();
|
||||
if(provider == null) {
|
||||
return;
|
||||
}
|
||||
final int position;
|
||||
if (isFromFeaturedRootFragment) {
|
||||
position = this.position;
|
||||
} else {
|
||||
position = binding.mediaDetailsPager.getCurrentItem();
|
||||
}
|
||||
|
||||
Media m = provider.getMediaAtPosition(position);
|
||||
if (m != null) {
|
||||
// Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
|
||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true);
|
||||
menu.findItem(R.id.menu_copy_link).setEnabled(true).setVisible(true);
|
||||
menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true);
|
||||
menu.findItem(R.id.menu_download_current_image).setEnabled(true).setVisible(true);
|
||||
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true).setVisible(true);
|
||||
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(true).setVisible(true);
|
||||
if (m.getUser() != null) {
|
||||
menu.findItem(R.id.menu_view_user_page).setEnabled(true).setVisible(true);
|
||||
}
|
||||
|
||||
try {
|
||||
URL mediaUrl = new URL(m.getImageUrl());
|
||||
this.handleBackgroundColorMenuItems(
|
||||
() -> BitmapFactory.decodeStream(mediaUrl.openConnection().getInputStream()),
|
||||
menu
|
||||
);
|
||||
} catch (Exception e) {
|
||||
Timber.e("Cant detect media transparency");
|
||||
}
|
||||
|
||||
// Initialize bookmark object
|
||||
bookmark = new Bookmark(
|
||||
m.getFilename(),
|
||||
m.getAuthorOrUser(),
|
||||
BookmarkPicturesContentProvider.uriForName(m.getFilename())
|
||||
);
|
||||
updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image));
|
||||
final Integer contributionState = provider.getContributionStateAt(position);
|
||||
if (contributionState != null) {
|
||||
switch (contributionState) {
|
||||
case Contribution.STATE_FAILED:
|
||||
case Contribution.STATE_IN_PROGRESS:
|
||||
case Contribution.STATE_QUEUED:
|
||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_copy_link).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
|
||||
.setVisible(false);
|
||||
break;
|
||||
case Contribution.STATE_COMPLETED:
|
||||
// Default set of menu items works fine. Treat same as regular media object
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_copy_link).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
|
||||
.setVisible(false);
|
||||
}
|
||||
|
||||
if (!sessionManager.isUserLoggedIn()) {
|
||||
menu.findItem(R.id.menu_set_as_avatar).setVisible(false);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide wether or not we should display the background color menu items
|
||||
* We display them if the image is transparent
|
||||
* @param getBitmap
|
||||
* @param menu
|
||||
*/
|
||||
private void handleBackgroundColorMenuItems(Callable<Bitmap> getBitmap, Menu menu) {
|
||||
Observable.fromCallable(
|
||||
getBitmap
|
||||
).subscribeOn(Schedulers.newThread())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(image -> {
|
||||
if (image.hasAlpha()) {
|
||||
menu.findItem(R.id.menu_view_set_white_background).setVisible(true).setEnabled(true);
|
||||
menu.findItem(R.id.menu_view_set_black_background).setVisible(true).setEnabled(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateBookmarkState(MenuItem item) {
|
||||
boolean isBookmarked = bookmarkDao.findBookmark(bookmark);
|
||||
if(isBookmarked) {
|
||||
if(removedItems.contains(binding.mediaDetailsPager.getCurrentItem())) {
|
||||
removedItems.remove(new Integer(binding.mediaDetailsPager.getCurrentItem()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(!removedItems.contains(binding.mediaDetailsPager.getCurrentItem())) {
|
||||
removedItems.add(binding.mediaDetailsPager.getCurrentItem());
|
||||
}
|
||||
}
|
||||
int icon = isBookmarked ? R.drawable.menu_ic_round_star_filled_24px : R.drawable.menu_ic_round_star_border_24px;
|
||||
item.setIcon(icon);
|
||||
}
|
||||
|
||||
public void showImage(int i, boolean isWikipediaButtonDisplayed) {
|
||||
this.isWikipediaButtonDisplayed = isWikipediaButtonDisplayed;
|
||||
setViewPagerCurrentItem(i);
|
||||
}
|
||||
|
||||
public void showImage(int i) {
|
||||
setViewPagerCurrentItem(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function waits for the item to load then sets the item to current item
|
||||
* @param position current item that to be shown
|
||||
*/
|
||||
private void setViewPagerCurrentItem(int position) {
|
||||
|
||||
final Handler handler = new Handler(Looper.getMainLooper());
|
||||
final Runnable runnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Show the ProgressBar while waiting for the item to load
|
||||
imageProgressBar.setVisibility(View.VISIBLE);
|
||||
// Check if the adapter has enough items loaded
|
||||
if(adapter.getCount() > position){
|
||||
// Set the current item in the ViewPager
|
||||
binding.mediaDetailsPager.setCurrentItem(position, false);
|
||||
// Hide the ProgressBar once the item is loaded
|
||||
imageProgressBar.setVisibility(View.GONE);
|
||||
} else {
|
||||
// If the item is not ready yet, post the Runnable again
|
||||
handler.post(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Start the Runnable
|
||||
handler.post(runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* The method notify the viewpager that number of items have changed.
|
||||
*/
|
||||
public void notifyDataSetChanged(){
|
||||
if (null != adapter) {
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int i, float v, int i2) {
|
||||
if(getActivity() == null) {
|
||||
Timber.d("Returning as activity is destroyed!");
|
||||
return;
|
||||
}
|
||||
|
||||
getActivity().invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int i) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int i) {
|
||||
}
|
||||
|
||||
public void onDataSetChanged() {
|
||||
if (null != adapter) {
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the media is nominated for deletion
|
||||
*
|
||||
* @param index item position that has been nominated
|
||||
*/
|
||||
@Override
|
||||
public void nominatingForDeletion(int index) {
|
||||
provider.refreshNominatedMedia(index);
|
||||
}
|
||||
|
||||
public interface MediaDetailProvider {
|
||||
Media getMediaAtPosition(int i);
|
||||
|
||||
int getTotalMediaCount();
|
||||
|
||||
Integer getContributionStateAt(int position);
|
||||
|
||||
// Reload media detail fragment once media is nominated
|
||||
void refreshNominatedMedia(int index);
|
||||
}
|
||||
|
||||
//FragmentStatePagerAdapter allows user to swipe across collection of images (no. of images undetermined)
|
||||
private class MediaDetailAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
/**
|
||||
* Keeps track of the current displayed fragment.
|
||||
*/
|
||||
private Fragment mCurrentFragment;
|
||||
|
||||
public MediaDetailAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int i) {
|
||||
if (i == 0) {
|
||||
// See bug https://code.google.com/p/android/issues/detail?id=27526
|
||||
if(getActivity() == null) {
|
||||
Timber.d("Skipping getItem. Returning as activity is destroyed!");
|
||||
return null;
|
||||
}
|
||||
binding.mediaDetailsPager.postDelayed(() -> getActivity().invalidateOptionsMenu(), 5);
|
||||
}
|
||||
if (isFromFeaturedRootFragment) {
|
||||
return MediaDetailFragment.forMedia(position+i, editable, isFeaturedImage, isWikipediaButtonDisplayed);
|
||||
} else {
|
||||
return MediaDetailFragment.forMedia(i, editable, isFeaturedImage, isWikipediaButtonDisplayed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
if (getActivity() == null) {
|
||||
Timber.d("Skipping getCount. Returning as activity is destroyed!");
|
||||
return 0;
|
||||
}
|
||||
return provider.getTotalMediaCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently displayed fragment.
|
||||
* @return
|
||||
*/
|
||||
public Fragment getCurrentFragment() {
|
||||
return mCurrentFragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* If current fragment is of type MediaDetailFragment, return it, otherwise return null.
|
||||
* @return MediaDetailFragment
|
||||
*/
|
||||
public MediaDetailFragment getCurrentMediaDetailFragment() {
|
||||
if (mCurrentFragment instanceof MediaDetailFragment) {
|
||||
return (MediaDetailFragment) mCurrentFragment;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to inform the adapter of which item is currently considered to be the "primary",
|
||||
* that is the one show to the user as the current page.
|
||||
* @param container
|
||||
* @param position
|
||||
* @param object
|
||||
*/
|
||||
@Override
|
||||
public void setPrimaryItem(@NonNull final ViewGroup container, final int position,
|
||||
@NonNull final Object object) {
|
||||
// Update the current fragment if changed
|
||||
if(getCurrentFragment() != object) {
|
||||
mCurrentFragment = ((Fragment)object);
|
||||
}
|
||||
super.setPrimaryItem(container, position, object);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,622 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.viewpager.widget.ViewPager.OnPageChangeListener
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.bookmarks.models.Bookmark
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
|
||||
import fr.free.nrw.commons.contributions.Contribution
|
||||
import fr.free.nrw.commons.contributions.MainActivity
|
||||
import fr.free.nrw.commons.databinding.FragmentMediaDetailPagerBinding
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||
import fr.free.nrw.commons.profile.ProfileActivity
|
||||
import fr.free.nrw.commons.profile.ProfileActivity.Companion.startYourself
|
||||
import fr.free.nrw.commons.utils.ClipboardUtils.copy
|
||||
import fr.free.nrw.commons.utils.DownloadUtils.downloadMedia
|
||||
import fr.free.nrw.commons.utils.ImageUtils.setAvatarFromImageUrl
|
||||
import fr.free.nrw.commons.utils.ImageUtils.setWallpaperFromImageUrl
|
||||
import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
|
||||
import fr.free.nrw.commons.utils.ViewUtil.showShortSnackbar
|
||||
import fr.free.nrw.commons.utils.handleWebUrl
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.functions.Consumer
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import java.net.URL
|
||||
import java.util.concurrent.Callable
|
||||
import javax.inject.Inject
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class MediaDetailPagerFragment : CommonsDaggerSupportFragment(), OnPageChangeListener,
|
||||
MediaDetailFragment.Callback {
|
||||
@JvmField
|
||||
@Inject
|
||||
var bookmarkDao: BookmarkPicturesDao? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var okHttpJsonApiClient: OkHttpJsonApiClient? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var sessionManager: SessionManager? = null
|
||||
|
||||
var binding: FragmentMediaDetailPagerBinding? = null
|
||||
var editable: Boolean = false
|
||||
var isFeaturedImage: Boolean = false
|
||||
var isWikipediaButtonDisplayed: Boolean = false
|
||||
var adapter: MediaDetailAdapter? = null
|
||||
var bookmark: Bookmark? = null
|
||||
var mediaDetailProvider: MediaDetailProvider? = null
|
||||
var isFromFeaturedRootFragment: Boolean = false
|
||||
var position: Int = 0
|
||||
|
||||
/**
|
||||
* ProgressBar used to indicate the loading status of media items.
|
||||
*/
|
||||
var imageProgressBar: ProgressBar? = null
|
||||
|
||||
var removedItems: ArrayList<Int> = ArrayList()
|
||||
|
||||
fun clearRemoved() = removedItems.clear()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = FragmentMediaDetailPagerBinding.inflate(inflater, container, false)
|
||||
binding!!.mediaDetailsPager.addOnPageChangeListener(this)
|
||||
// Initialize the ProgressBar by finding it in the layout
|
||||
imageProgressBar = binding!!.root.findViewById(R.id.itemProgressBar)
|
||||
adapter = MediaDetailAdapter(this, childFragmentManager)
|
||||
|
||||
// ActionBar is now supported in both activities - if this crashes something is quite wrong
|
||||
val actionBar = (activity as AppCompatActivity).supportActionBar
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true)
|
||||
} else {
|
||||
throw AssertionError("Action bar should not be null!")
|
||||
}
|
||||
|
||||
// If fragment is associated with ProfileActivity, then hide the tabLayout
|
||||
if (activity is ProfileActivity) {
|
||||
(activity as ProfileActivity).setTabLayoutVisibility(false)
|
||||
} else if (activity is MainActivity) {
|
||||
(activity as MainActivity).hideTabs()
|
||||
}
|
||||
|
||||
binding!!.mediaDetailsPager.adapter = adapter
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
val pageNumber = savedInstanceState.getInt("current-page")
|
||||
binding!!.mediaDetailsPager.setCurrentItem(pageNumber, false)
|
||||
requireActivity().invalidateOptionsMenu()
|
||||
}
|
||||
adapter!!.notifyDataSetChanged()
|
||||
|
||||
return binding!!.root
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putInt("current-page", binding!!.mediaDetailsPager.currentItem)
|
||||
outState.putBoolean("editable", editable)
|
||||
outState.putBoolean("isFeaturedImage", isFeaturedImage)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (savedInstanceState != null) {
|
||||
editable = savedInstanceState.getBoolean("editable", false)
|
||||
isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage", false)
|
||||
}
|
||||
setHasOptionsMenu(true)
|
||||
initProvider()
|
||||
}
|
||||
|
||||
/**
|
||||
* initialise the provider, based on from where the fragment was started, as in from an activity
|
||||
* or a fragment
|
||||
*/
|
||||
private fun initProvider() {
|
||||
if (parentFragment is MediaDetailProvider) {
|
||||
mediaDetailProvider = parentFragment as MediaDetailProvider
|
||||
} else if (activity is MediaDetailProvider) {
|
||||
mediaDetailProvider = activity as MediaDetailProvider?
|
||||
} else {
|
||||
throw ClassCastException("Parent must implement MediaDetailProvider")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (activity == null) {
|
||||
Timber.d("Returning as activity is destroyed!")
|
||||
return true
|
||||
}
|
||||
|
||||
val m = mediaDetailProvider!!.getMediaAtPosition(binding!!.mediaDetailsPager.currentItem)
|
||||
val mediaDetailFragment = adapter!!.currentMediaDetailFragment
|
||||
when (item.itemId) {
|
||||
R.id.menu_bookmark_current_image -> {
|
||||
val bookmarkExists = bookmarkDao!!.updateBookmark(bookmark)
|
||||
val snackbar = if (bookmarkExists) Snackbar.make(
|
||||
requireView(),
|
||||
R.string.add_bookmark,
|
||||
Snackbar.LENGTH_LONG
|
||||
) else Snackbar.make(
|
||||
requireView(), R.string.remove_bookmark, Snackbar.LENGTH_LONG
|
||||
)
|
||||
snackbar.show()
|
||||
updateBookmarkState(item)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_copy_link -> {
|
||||
val uri = m!!.pageTitle.canonicalUri
|
||||
copy("shareLink", uri, requireContext())
|
||||
Timber.d("Copied share link to clipboard: %s", uri)
|
||||
Toast.makeText(
|
||||
requireContext(), getString(R.string.menu_link_copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_share_current_image -> {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.setType("text/plain")
|
||||
shareIntent.putExtra(
|
||||
Intent.EXTRA_TEXT, """${m!!.displayTitle}
|
||||
${m.pageTitle.canonicalUri}"""
|
||||
)
|
||||
startActivity(Intent.createChooser(shareIntent, "Share image via..."))
|
||||
|
||||
//Add media detail to backstack when the share button is clicked
|
||||
//So that when the share is cancelled or completed the media detail page is on top
|
||||
// of back stack fixing:https://github.com/commons-app/apps-android-commons/issues/2296
|
||||
val supportFragmentManager = requireActivity().supportFragmentManager
|
||||
if (supportFragmentManager.backStackEntryCount < 2) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.addToBackStack(MediaDetailPagerFragment::class.java.name)
|
||||
.commit()
|
||||
supportFragmentManager.executePendingTransactions()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_browser_current_image -> {
|
||||
// View in browser
|
||||
handleWebUrl(requireContext(), m!!.pageTitle.mobileUri.toUri())
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_download_current_image -> {
|
||||
// Download
|
||||
if (!isInternetConnectionEstablished(activity)) {
|
||||
showShortSnackbar(requireView(), R.string.no_internet)
|
||||
return false
|
||||
}
|
||||
downloadMedia(activity, m!!)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_set_as_wallpaper -> {
|
||||
// Set wallpaper
|
||||
setWallpaper(m!!)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_set_as_avatar -> {
|
||||
// Set avatar
|
||||
setAvatar(m!!)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_view_user_page -> {
|
||||
if (m?.user != null) {
|
||||
startYourself(
|
||||
requireActivity(), m.user!!,
|
||||
sessionManager!!.userName != m.user
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_view_report -> {
|
||||
showReportDialog(m)
|
||||
mediaDetailFragment?.onImageBackgroundChanged(
|
||||
ContextCompat.getColor(
|
||||
requireContext(),
|
||||
R.color.white
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_view_set_white_background -> {
|
||||
mediaDetailFragment?.onImageBackgroundChanged(
|
||||
ContextCompat.getColor(
|
||||
requireContext(),
|
||||
R.color.white
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_view_set_black_background -> {
|
||||
mediaDetailFragment?.onImageBackgroundChanged(
|
||||
ContextCompat.getColor(
|
||||
requireContext(),
|
||||
R.color.black
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showReportDialog(media: Media?) {
|
||||
if (media == null) {
|
||||
return
|
||||
}
|
||||
val builder = AlertDialog.Builder(requireActivity())
|
||||
val values = requireContext().resources
|
||||
.getStringArray(R.array.report_violation_options)
|
||||
builder.setTitle(R.string.report_violation)
|
||||
builder.setItems(
|
||||
R.array.report_violation_options
|
||||
) { dialog: DialogInterface?, which: Int ->
|
||||
sendReportEmail(media, values[which])
|
||||
}
|
||||
builder.setNegativeButton(
|
||||
R.string.cancel
|
||||
) { dialog: DialogInterface?, which: Int -> }
|
||||
builder.setCancelable(false)
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun sendReportEmail(media: Media, type: String) {
|
||||
val technicalInfo = getTechInfo(media, type)
|
||||
|
||||
val feedbackIntent = Intent(Intent.ACTION_SENDTO)
|
||||
feedbackIntent.setType("message/rfc822")
|
||||
feedbackIntent.setData(Uri.parse("mailto:"))
|
||||
feedbackIntent.putExtra(
|
||||
Intent.EXTRA_EMAIL,
|
||||
arrayOf(CommonsApplication.REPORT_EMAIL)
|
||||
)
|
||||
feedbackIntent.putExtra(
|
||||
Intent.EXTRA_SUBJECT,
|
||||
CommonsApplication.REPORT_EMAIL_SUBJECT
|
||||
)
|
||||
feedbackIntent.putExtra(Intent.EXTRA_TEXT, technicalInfo)
|
||||
try {
|
||||
startActivity(feedbackIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(activity, R.string.no_email_client, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTechInfo(media: Media, type: String): String {
|
||||
val builder = StringBuilder()
|
||||
|
||||
builder.append("Report type: ")
|
||||
.append(type)
|
||||
.append("\n\n")
|
||||
|
||||
builder.append("Image that you want to report: ")
|
||||
.append(media.imageUrl)
|
||||
.append("\n\n")
|
||||
|
||||
builder.append("User that you want to report: ")
|
||||
.append(media.user)
|
||||
.append("\n\n")
|
||||
|
||||
if (sessionManager!!.userName != null) {
|
||||
builder.append("Your username: ")
|
||||
.append(sessionManager!!.userName)
|
||||
.append("\n\n")
|
||||
}
|
||||
|
||||
builder.append("Violation reason: ")
|
||||
.append("\n")
|
||||
|
||||
builder.append("----------------------------------------------")
|
||||
.append("\n")
|
||||
.append("(please write reason here)")
|
||||
.append("\n")
|
||||
.append("----------------------------------------------")
|
||||
.append("\n\n")
|
||||
.append("Thank you for your report! Our team will investigate as soon as possible.")
|
||||
.append("\n")
|
||||
.append("Please note that images also have a `Nominate for deletion` button.")
|
||||
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the media as the device's wallpaper if the imageUrl is not null
|
||||
* Fails silently if setting the wallpaper fails
|
||||
* @param media
|
||||
*/
|
||||
private fun setWallpaper(media: Media) {
|
||||
if (media.imageUrl == null || media.imageUrl!!.isEmpty()) {
|
||||
Timber.d("Media URL not present")
|
||||
return
|
||||
}
|
||||
setWallpaperFromImageUrl(requireActivity(), media.imageUrl!!.toUri())
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the media as user's leaderboard avatar
|
||||
* @param media
|
||||
*/
|
||||
private fun setAvatar(media: Media) {
|
||||
if (media.imageUrl == null || media.imageUrl!!.isEmpty()) {
|
||||
Timber.d("Media URL not present")
|
||||
return
|
||||
}
|
||||
setAvatarFromImageUrl(
|
||||
requireActivity(), media.imageUrl!!,
|
||||
sessionManager!!.currentAccount!!.name,
|
||||
okHttpJsonApiClient!!, Companion.compositeDisposable
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
if (!editable) { // Disable menu options for editable views
|
||||
menu.clear() // see http://stackoverflow.com/a/8495697/17865
|
||||
inflater.inflate(R.menu.fragment_image_detail, menu)
|
||||
if (binding!!.mediaDetailsPager != null) {
|
||||
val provider = mediaDetailProvider ?: return
|
||||
val position = if (isFromFeaturedRootFragment) {
|
||||
position
|
||||
} else {
|
||||
binding!!.mediaDetailsPager.currentItem
|
||||
}
|
||||
|
||||
val m = provider.getMediaAtPosition(position)
|
||||
if (m != null) {
|
||||
// Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
|
||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true)
|
||||
menu.findItem(R.id.menu_copy_link).setEnabled(true).setVisible(true)
|
||||
menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true)
|
||||
menu.findItem(R.id.menu_download_current_image).setEnabled(true)
|
||||
.setVisible(true)
|
||||
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true)
|
||||
.setVisible(true)
|
||||
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(true).setVisible(true)
|
||||
if (m.user != null) {
|
||||
menu.findItem(R.id.menu_view_user_page).setEnabled(true).setVisible(true)
|
||||
}
|
||||
|
||||
try {
|
||||
val mediaUrl = URL(m.imageUrl)
|
||||
handleBackgroundColorMenuItems({
|
||||
BitmapFactory.decodeStream(
|
||||
mediaUrl.openConnection().getInputStream()
|
||||
)
|
||||
}, menu)
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Cant detect media transparency")
|
||||
}
|
||||
|
||||
// Initialize bookmark object
|
||||
bookmark = Bookmark(
|
||||
m.filename,
|
||||
m.getAuthorOrUser(),
|
||||
BookmarkPicturesContentProvider.uriForName(m.filename)
|
||||
)
|
||||
updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image))
|
||||
val contributionState = provider.getContributionStateAt(position)
|
||||
if (contributionState != null) {
|
||||
when (contributionState) {
|
||||
Contribution.STATE_FAILED, Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED -> {
|
||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_copy_link).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
|
||||
.setVisible(false)
|
||||
}
|
||||
|
||||
Contribution.STATE_COMPLETED -> {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_copy_link).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
|
||||
.setVisible(false)
|
||||
}
|
||||
|
||||
if (!sessionManager!!.isUserLoggedIn) {
|
||||
menu.findItem(R.id.menu_set_as_avatar).setVisible(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide wether or not we should display the background color menu items
|
||||
* We display them if the image is transparent
|
||||
* @param getBitmap
|
||||
* @param menu
|
||||
*/
|
||||
private fun handleBackgroundColorMenuItems(getBitmap: Callable<Bitmap>, menu: Menu) {
|
||||
Observable.fromCallable(
|
||||
getBitmap
|
||||
).subscribeOn(Schedulers.newThread())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(Consumer { image: Bitmap ->
|
||||
if (image.hasAlpha()) {
|
||||
menu.findItem(R.id.menu_view_set_white_background).setVisible(true)
|
||||
.setEnabled(true)
|
||||
menu.findItem(R.id.menu_view_set_black_background).setVisible(true)
|
||||
.setEnabled(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateBookmarkState(item: MenuItem) {
|
||||
val isBookmarked = bookmarkDao!!.findBookmark(bookmark)
|
||||
if (isBookmarked) {
|
||||
if (removedItems.contains(binding!!.mediaDetailsPager.currentItem)) {
|
||||
removedItems.remove(binding!!.mediaDetailsPager.currentItem)
|
||||
}
|
||||
} else {
|
||||
if (!removedItems.contains(binding!!.mediaDetailsPager.currentItem)) {
|
||||
removedItems.add(binding!!.mediaDetailsPager.currentItem)
|
||||
}
|
||||
}
|
||||
|
||||
item.setIcon(if (isBookmarked) {
|
||||
R.drawable.menu_ic_round_star_filled_24px
|
||||
} else {
|
||||
R.drawable.menu_ic_round_star_border_24px
|
||||
})
|
||||
}
|
||||
|
||||
fun showImage(i: Int, isWikipediaButtonDisplayed: Boolean) {
|
||||
this.isWikipediaButtonDisplayed = isWikipediaButtonDisplayed
|
||||
setViewPagerCurrentItem(i)
|
||||
}
|
||||
|
||||
fun showImage(i: Int) {
|
||||
setViewPagerCurrentItem(i)
|
||||
}
|
||||
|
||||
/**
|
||||
* This function waits for the item to load then sets the item to current item
|
||||
* @param position current item that to be shown
|
||||
*/
|
||||
private fun setViewPagerCurrentItem(position: Int) {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
val runnable: Runnable = object : Runnable {
|
||||
override fun run() {
|
||||
// Show the ProgressBar while waiting for the item to load
|
||||
imageProgressBar!!.visibility = View.VISIBLE
|
||||
// Check if the adapter has enough items loaded
|
||||
if (adapter!!.count > position) {
|
||||
// Set the current item in the ViewPager
|
||||
binding!!.mediaDetailsPager.setCurrentItem(position, false)
|
||||
// Hide the ProgressBar once the item is loaded
|
||||
imageProgressBar!!.visibility = View.GONE
|
||||
} else {
|
||||
// If the item is not ready yet, post the Runnable again
|
||||
handler.post(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start the Runnable
|
||||
handler.post(runnable)
|
||||
}
|
||||
|
||||
/**
|
||||
* The method notify the viewpager that number of items have changed.
|
||||
*/
|
||||
fun notifyDataSetChanged() {
|
||||
if (null != adapter) {
|
||||
adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageScrolled(i: Int, v: Float, i2: Int) {
|
||||
if (activity == null) {
|
||||
Timber.d("Returning as activity is destroyed!")
|
||||
return
|
||||
}
|
||||
|
||||
requireActivity().invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onPageSelected(i: Int) {
|
||||
}
|
||||
|
||||
override fun onPageScrollStateChanged(i: Int) {
|
||||
}
|
||||
|
||||
fun onDataSetChanged() {
|
||||
if (null != adapter) {
|
||||
adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the media is nominated for deletion
|
||||
*
|
||||
* @param index item position that has been nominated
|
||||
*/
|
||||
override fun nominatingForDeletion(index: Int) {
|
||||
mediaDetailProvider!!.refreshNominatedMedia(index)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
/**
|
||||
* Use this factory method to create a new instance of this fragment using the provided
|
||||
* parameters.
|
||||
*
|
||||
* This method will create a new instance of MediaDetailPagerFragment and the arguments will be
|
||||
* saved to a bundle which will be later available in the [.onCreate]
|
||||
* @param editable
|
||||
* @param isFeaturedImage
|
||||
* @return
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newInstance(editable: Boolean, isFeaturedImage: Boolean): MediaDetailPagerFragment {
|
||||
val mediaDetailPagerFragment = MediaDetailPagerFragment()
|
||||
val args = Bundle()
|
||||
args.putBoolean("is_editable", editable)
|
||||
args.putBoolean("is_featured_image", isFeaturedImage)
|
||||
mediaDetailPagerFragment.arguments = args
|
||||
return mediaDetailPagerFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import fr.free.nrw.commons.Media
|
||||
|
||||
interface MediaDetailProvider {
|
||||
fun getMediaAtPosition(i: Int): Media?
|
||||
|
||||
fun getTotalMediaCount(): Int
|
||||
|
||||
fun getContributionStateAt(position: Int): Int?
|
||||
|
||||
// Reload media detail fragment once media is nominated
|
||||
fun refreshNominatedMedia(index: Int)
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwResponse;
|
||||
|
||||
public class MwParseResponse extends MwResponse {
|
||||
@Nullable
|
||||
private MwParseResult parse;
|
||||
|
||||
@Nullable
|
||||
public MwParseResult parse() {
|
||||
return parse;
|
||||
}
|
||||
|
||||
public boolean success() {
|
||||
return parse != null;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected void setParse(@Nullable MwParseResult parse) {
|
||||
this.parse = parse;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwResponse
|
||||
|
||||
class MwParseResponse : MwResponse() {
|
||||
private var parse: MwParseResult? = null
|
||||
|
||||
fun parse(): MwParseResult? = parse
|
||||
|
||||
fun success(): Boolean = parse != null
|
||||
|
||||
@VisibleForTesting
|
||||
protected fun setParse(parse: MwParseResult?) {
|
||||
this.parse = parse
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class MwParseResult {
|
||||
@SuppressWarnings("unused") private int pageid;
|
||||
@SuppressWarnings("unused") private int index;
|
||||
private MwParseText text;
|
||||
|
||||
public String text() {
|
||||
return text.text;
|
||||
}
|
||||
|
||||
|
||||
public class MwParseText{
|
||||
@SerializedName("*") private String text;
|
||||
}
|
||||
}
|
||||
18
app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt
Normal file
18
app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class MwParseResult {
|
||||
private val pageid = 0
|
||||
private val index = 0
|
||||
private val text: MwParseText? = null
|
||||
|
||||
fun text(): String? {
|
||||
return text?.text
|
||||
}
|
||||
|
||||
inner class MwParseText {
|
||||
@SerializedName("*")
|
||||
internal val text: String? = null
|
||||
}
|
||||
}
|
||||
|
|
@ -74,6 +74,7 @@ import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType
|
|||
import fr.free.nrw.commons.location.LocationUpdateListener
|
||||
import fr.free.nrw.commons.media.MediaClient
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment
|
||||
import fr.free.nrw.commons.media.MediaDetailProvider
|
||||
import fr.free.nrw.commons.navtab.NavTab
|
||||
import fr.free.nrw.commons.nearby.BottomSheetAdapter
|
||||
import fr.free.nrw.commons.nearby.BottomSheetAdapter.ItemClickListener
|
||||
|
|
@ -150,7 +151,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
|
|||
LocationUpdateListener,
|
||||
LocationPermissionCallback,
|
||||
ItemClickListener,
|
||||
MediaDetailPagerFragment.MediaDetailProvider {
|
||||
MediaDetailProvider {
|
||||
var binding: FragmentNearbyParentBinding? = null
|
||||
|
||||
val mapEventsOverlay: MapEventsOverlay = MapEventsOverlay(object : MapEventsReceiver {
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ class BookmarkListRootFragmentUnitTest {
|
|||
@Throws(Exception::class)
|
||||
fun testGetTotalMediaCountCaseNull() {
|
||||
whenever(bookmarksPagerAdapter.mediaAdapter).thenReturn(null)
|
||||
Assert.assertEquals(fragment.totalMediaCount, 0)
|
||||
Assert.assertEquals(fragment.getTotalMediaCount(), 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -244,7 +244,7 @@ class BookmarkListRootFragmentUnitTest {
|
|||
val listAdapter = mock(ListAdapter::class.java)
|
||||
whenever(bookmarksPagerAdapter.mediaAdapter).thenReturn(listAdapter)
|
||||
whenever(listAdapter.count).thenReturn(1)
|
||||
Assert.assertEquals(fragment.totalMediaCount, 1)
|
||||
Assert.assertEquals(fragment.getTotalMediaCount(), 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class CategoryDetailsActivityUnitTests {
|
|||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetTotalMediaCount() {
|
||||
activity.totalMediaCount
|
||||
activity.getTotalMediaCount()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ class ContributionsFragmentUnitTests {
|
|||
@Throws(Exception::class)
|
||||
fun testGetTotalMediaCount() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
fragment.totalMediaCount
|
||||
fragment.getTotalMediaCount()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -189,8 +189,8 @@ class ExploreListRootFragmentUnitTest {
|
|||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetTotalMediaCount() {
|
||||
`when`(listFragment.totalMediaCount).thenReturn(1)
|
||||
Assert.assertEquals(fragment.totalMediaCount, 1)
|
||||
`when`(listFragment.getTotalMediaCount()).thenReturn(1)
|
||||
Assert.assertEquals(fragment.getTotalMediaCount(), 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -199,7 +199,7 @@ class ExploreListRootFragmentUnitTest {
|
|||
val field: Field = ExploreListRootFragment::class.java.getDeclaredField("listFragment")
|
||||
field.isAccessible = true
|
||||
field.set(fragment, null)
|
||||
Assert.assertEquals(fragment.totalMediaCount, 0)
|
||||
Assert.assertEquals(fragment.getTotalMediaCount(), 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ class WikidataItemDetailsActivityUnitTests {
|
|||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetTotalMediaCount() {
|
||||
activity.totalMediaCount
|
||||
activity.getTotalMediaCount()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -160,8 +160,8 @@ class SearchActivityUnitTests {
|
|||
fun testGetTotalMediaCount() {
|
||||
val num = 1
|
||||
Whitebox.setInternalState(activity, "searchMediaFragment", searchMediaFragment)
|
||||
`when`(searchMediaFragment.totalMediaCount).thenReturn(num)
|
||||
assertEquals(activity.totalMediaCount, num)
|
||||
`when`(searchMediaFragment.getTotalMediaCount()).thenReturn(num)
|
||||
assertEquals(activity.getTotalMediaCount(), num)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import java.util.concurrent.Executor
|
|||
class CustomOkHttpNetworkFetcherUnitTest {
|
||||
private lateinit var fetcher: CustomOkHttpNetworkFetcher
|
||||
private lateinit var okHttpClient: OkHttpClient
|
||||
private lateinit var state: CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState
|
||||
private lateinit var state: OkHttpNetworkFetchState
|
||||
|
||||
@Mock
|
||||
private lateinit var callback: NetworkFetcher.Callback
|
||||
|
|
@ -162,7 +162,7 @@ class CustomOkHttpNetworkFetcherUnitTest {
|
|||
val method: Method =
|
||||
CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod(
|
||||
"onFetchResponse",
|
||||
CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState::class.java,
|
||||
OkHttpNetworkFetchState::class.java,
|
||||
Call::class.java,
|
||||
Response::class.java,
|
||||
NetworkFetcher.Callback::class.java,
|
||||
|
|
@ -196,7 +196,7 @@ class CustomOkHttpNetworkFetcherUnitTest {
|
|||
val method: Method =
|
||||
CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod(
|
||||
"onFetchResponse",
|
||||
CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState::class.java,
|
||||
OkHttpNetworkFetchState::class.java,
|
||||
Call::class.java,
|
||||
Response::class.java,
|
||||
NetworkFetcher.Callback::class.java,
|
||||
|
|
@ -230,7 +230,7 @@ class CustomOkHttpNetworkFetcherUnitTest {
|
|||
val method: Method =
|
||||
CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod(
|
||||
"onFetchResponse",
|
||||
CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState::class.java,
|
||||
OkHttpNetworkFetchState::class.java,
|
||||
Call::class.java,
|
||||
Response::class.java,
|
||||
NetworkFetcher.Callback::class.java,
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ class MediaDetailFragmentUnitTests {
|
|||
private lateinit var button: Button
|
||||
|
||||
@Mock
|
||||
private lateinit var detailProvider: MediaDetailPagerFragment.MediaDetailProvider
|
||||
private lateinit var detailProvider: MediaDetailProvider
|
||||
|
||||
@Mock
|
||||
private lateinit var applicationKvStore: JsonKvStore
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue