Convert media package to kotlin (#6369)
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:
Paul Hawke 2025-07-11 21:11:20 -05:00 committed by GitHub
parent 79f52db929
commit 8fc7e1039b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 1030 additions and 1184 deletions

View file

@ -22,6 +22,7 @@ import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding; import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment; 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.navtab.NavTab;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
@ -29,7 +30,7 @@ import timber.log.Timber;
public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements
FragmentManager.OnBackStackChangedListener, FragmentManager.OnBackStackChangedListener,
MediaDetailPagerFragment.MediaDetailProvider, MediaDetailProvider,
AdapterView.OnItemClickListener, CategoryImagesCallback { AdapterView.OnItemClickListener, CategoryImagesCallback {
private MediaDetailPagerFragment mediaDetails; private MediaDetailPagerFragment mediaDetails;

View file

@ -8,7 +8,6 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope 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.parent.ParentCategoriesFragment
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment
import fr.free.nrw.commons.media.MediaDetailPagerFragment 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.theme.BaseActivity
import fr.free.nrw.commons.utils.handleWebUrl import fr.free.nrw.commons.utils.handleWebUrl
import fr.free.nrw.commons.wikidata.model.WikiSite import fr.free.nrw.commons.wikidata.model.WikiSite
@ -36,7 +36,7 @@ import javax.inject.Inject
* a particular category on wikimedia commons. * a particular category on wikimedia commons.
*/ */
class CategoryDetailsActivity : BaseActivity(), class CategoryDetailsActivity : BaseActivity(),
MediaDetailPagerFragment.MediaDetailProvider, MediaDetailProvider,
CategoryImagesCallback { CategoryImagesCallback {
private lateinit var supportFragmentManager: FragmentManager private lateinit var supportFragmentManager: FragmentManager

View file

@ -43,7 +43,7 @@ import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.location.LocationServiceManager import fr.free.nrw.commons.location.LocationServiceManager
import fr.free.nrw.commons.location.LocationUpdateListener import fr.free.nrw.commons.location.LocationUpdateListener
import fr.free.nrw.commons.media.MediaDetailPagerFragment 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.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.nearby.NearbyController import fr.free.nrw.commons.nearby.NearbyController
import fr.free.nrw.commons.nearby.NearbyNotificationCardView import fr.free.nrw.commons.nearby.NearbyNotificationCardView
@ -72,7 +72,6 @@ import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import java.util.Calendar import java.util.Calendar
import java.util.Date
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Named import javax.inject.Named

View file

@ -5,7 +5,6 @@ import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; 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.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment; import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment; 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.navtab.NavTab;
public class ExploreListRootFragment extends CommonsDaggerSupportFragment implements public class ExploreListRootFragment extends CommonsDaggerSupportFragment implements
MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback { MediaDetailProvider, CategoryImagesCallback {
private MediaDetailPagerFragment mediaDetails; private MediaDetailPagerFragment mediaDetails;
private CategoriesMediaFragment listFragment; private CategoriesMediaFragment listFragment;

View file

@ -5,7 +5,6 @@ import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; 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.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.map.ExploreMapFragment; import fr.free.nrw.commons.explore.map.ExploreMapFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment; 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.navtab.NavTab;
public class ExploreMapRootFragment extends CommonsDaggerSupportFragment implements public class ExploreMapRootFragment extends CommonsDaggerSupportFragment implements
MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback { MediaDetailProvider, CategoryImagesCallback {
private MediaDetailPagerFragment mediaDetails; private MediaDetailPagerFragment mediaDetails;
private ExploreMapFragment mapFragment; private ExploreMapFragment mapFragment;

View file

@ -6,7 +6,6 @@ import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
import com.jakewharton.rxbinding2.view.RxView; 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.RecentSearchesDao;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment; import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment; 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.theme.BaseActivity;
import fr.free.nrw.commons.utils.FragmentUtils; import fr.free.nrw.commons.utils.FragmentUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.inject.Inject; import javax.inject.Inject;
import kotlin.Pair;
import timber.log.Timber; import timber.log.Timber;
/** /**
@ -42,7 +37,7 @@ import timber.log.Timber;
*/ */
public class SearchActivity extends BaseActivity public class SearchActivity extends BaseActivity
implements MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback { implements MediaDetailProvider, CategoryImagesCallback {
@Inject @Inject
RecentSearchesDao recentSearchesDao; RecentSearchesDao recentSearchesDao;

View file

@ -11,7 +11,6 @@ import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import fr.free.nrw.commons.Media; 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.media.DepictedImagesFragment;
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment; import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment; 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.theme.BaseActivity;
import fr.free.nrw.commons.upload.structure.depictions.DepictModel; import fr.free.nrw.commons.upload.structure.depictions.DepictModel;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; 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.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import kotlin.Pair;
/** /**
* Activity to show depiction media, parent classes and child classes of depicted items in Explore * 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 { CategoryImagesCallback {
private FragmentManager supportFragmentManager; private FragmentManager supportFragmentManager;
private DepictedImagesFragment depictionImagesListFragment; private DepictedImagesFragment depictionImagesListFragment;

View file

@ -8,7 +8,7 @@ import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.R import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryImagesCallback import fr.free.nrw.commons.category.CategoryImagesCallback
import fr.free.nrw.commons.explore.paging.BasePagingFragment 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 import javax.inject.Inject
abstract class PageableMediaFragment : abstract class PageableMediaFragment :

View file

@ -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;
}
}

View 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
}
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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"
}
}

View file

@ -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)
}
}

View file

@ -77,7 +77,6 @@ import fr.free.nrw.commons.CommonsApplication.Companion.instance
import fr.free.nrw.commons.Media import fr.free.nrw.commons.Media
import fr.free.nrw.commons.MediaDataExtractor import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.R 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.actions.ThanksClient
import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException 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.language.AppLanguageLookUpTable
import fr.free.nrw.commons.location.LocationServiceManager import fr.free.nrw.commons.location.LocationServiceManager
import fr.free.nrw.commons.locationpicker.LocationPicker 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.profile.ProfileActivity
import fr.free.nrw.commons.review.ReviewHelper import fr.free.nrw.commons.review.ReviewHelper
import fr.free.nrw.commons.settings.Prefs import fr.free.nrw.commons.settings.Prefs

View file

@ -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);
}
}
}

View file

@ -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
}
}
}

View file

@ -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)
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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;
}
}

View 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
}
}

View file

@ -74,6 +74,7 @@ import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType
import fr.free.nrw.commons.location.LocationUpdateListener import fr.free.nrw.commons.location.LocationUpdateListener
import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.media.MediaDetailPagerFragment 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.navtab.NavTab
import fr.free.nrw.commons.nearby.BottomSheetAdapter import fr.free.nrw.commons.nearby.BottomSheetAdapter
import fr.free.nrw.commons.nearby.BottomSheetAdapter.ItemClickListener import fr.free.nrw.commons.nearby.BottomSheetAdapter.ItemClickListener
@ -150,7 +151,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
LocationUpdateListener, LocationUpdateListener,
LocationPermissionCallback, LocationPermissionCallback,
ItemClickListener, ItemClickListener,
MediaDetailPagerFragment.MediaDetailProvider { MediaDetailProvider {
var binding: FragmentNearbyParentBinding? = null var binding: FragmentNearbyParentBinding? = null
val mapEventsOverlay: MapEventsOverlay = MapEventsOverlay(object : MapEventsReceiver { val mapEventsOverlay: MapEventsOverlay = MapEventsOverlay(object : MapEventsReceiver {

View file

@ -235,7 +235,7 @@ class BookmarkListRootFragmentUnitTest {
@Throws(Exception::class) @Throws(Exception::class)
fun testGetTotalMediaCountCaseNull() { fun testGetTotalMediaCountCaseNull() {
whenever(bookmarksPagerAdapter.mediaAdapter).thenReturn(null) whenever(bookmarksPagerAdapter.mediaAdapter).thenReturn(null)
Assert.assertEquals(fragment.totalMediaCount, 0) Assert.assertEquals(fragment.getTotalMediaCount(), 0)
} }
@Test @Test
@ -244,7 +244,7 @@ class BookmarkListRootFragmentUnitTest {
val listAdapter = mock(ListAdapter::class.java) val listAdapter = mock(ListAdapter::class.java)
whenever(bookmarksPagerAdapter.mediaAdapter).thenReturn(listAdapter) whenever(bookmarksPagerAdapter.mediaAdapter).thenReturn(listAdapter)
whenever(listAdapter.count).thenReturn(1) whenever(listAdapter.count).thenReturn(1)
Assert.assertEquals(fragment.totalMediaCount, 1) Assert.assertEquals(fragment.getTotalMediaCount(), 1)
} }
@Test @Test

View file

@ -76,7 +76,7 @@ class CategoryDetailsActivityUnitTests {
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetTotalMediaCount() { fun testGetTotalMediaCount() {
activity.totalMediaCount activity.getTotalMediaCount()
} }
@Test @Test

View file

@ -334,7 +334,7 @@ class ContributionsFragmentUnitTests {
@Throws(Exception::class) @Throws(Exception::class)
fun testGetTotalMediaCount() { fun testGetTotalMediaCount() {
Shadows.shadowOf(Looper.getMainLooper()).idle() Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.totalMediaCount fragment.getTotalMediaCount()
} }
@Test @Test

View file

@ -189,8 +189,8 @@ class ExploreListRootFragmentUnitTest {
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetTotalMediaCount() { fun testGetTotalMediaCount() {
`when`(listFragment.totalMediaCount).thenReturn(1) `when`(listFragment.getTotalMediaCount()).thenReturn(1)
Assert.assertEquals(fragment.totalMediaCount, 1) Assert.assertEquals(fragment.getTotalMediaCount(), 1)
} }
@Test @Test
@ -199,7 +199,7 @@ class ExploreListRootFragmentUnitTest {
val field: Field = ExploreListRootFragment::class.java.getDeclaredField("listFragment") val field: Field = ExploreListRootFragment::class.java.getDeclaredField("listFragment")
field.isAccessible = true field.isAccessible = true
field.set(fragment, null) field.set(fragment, null)
Assert.assertEquals(fragment.totalMediaCount, 0) Assert.assertEquals(fragment.getTotalMediaCount(), 0)
} }
@Test @Test

View file

@ -108,7 +108,7 @@ class WikidataItemDetailsActivityUnitTests {
@Test @Test
@Throws(Exception::class) @Throws(Exception::class)
fun testGetTotalMediaCount() { fun testGetTotalMediaCount() {
activity.totalMediaCount activity.getTotalMediaCount()
} }
@Test @Test

View file

@ -160,8 +160,8 @@ class SearchActivityUnitTests {
fun testGetTotalMediaCount() { fun testGetTotalMediaCount() {
val num = 1 val num = 1
Whitebox.setInternalState(activity, "searchMediaFragment", searchMediaFragment) Whitebox.setInternalState(activity, "searchMediaFragment", searchMediaFragment)
`when`(searchMediaFragment.totalMediaCount).thenReturn(num) `when`(searchMediaFragment.getTotalMediaCount()).thenReturn(num)
assertEquals(activity.totalMediaCount, num) assertEquals(activity.getTotalMediaCount(), num)
} }
@Test @Test

View file

@ -32,7 +32,7 @@ import java.util.concurrent.Executor
class CustomOkHttpNetworkFetcherUnitTest { class CustomOkHttpNetworkFetcherUnitTest {
private lateinit var fetcher: CustomOkHttpNetworkFetcher private lateinit var fetcher: CustomOkHttpNetworkFetcher
private lateinit var okHttpClient: OkHttpClient private lateinit var okHttpClient: OkHttpClient
private lateinit var state: CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState private lateinit var state: OkHttpNetworkFetchState
@Mock @Mock
private lateinit var callback: NetworkFetcher.Callback private lateinit var callback: NetworkFetcher.Callback
@ -162,7 +162,7 @@ class CustomOkHttpNetworkFetcherUnitTest {
val method: Method = val method: Method =
CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod( CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod(
"onFetchResponse", "onFetchResponse",
CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState::class.java, OkHttpNetworkFetchState::class.java,
Call::class.java, Call::class.java,
Response::class.java, Response::class.java,
NetworkFetcher.Callback::class.java, NetworkFetcher.Callback::class.java,
@ -196,7 +196,7 @@ class CustomOkHttpNetworkFetcherUnitTest {
val method: Method = val method: Method =
CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod( CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod(
"onFetchResponse", "onFetchResponse",
CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState::class.java, OkHttpNetworkFetchState::class.java,
Call::class.java, Call::class.java,
Response::class.java, Response::class.java,
NetworkFetcher.Callback::class.java, NetworkFetcher.Callback::class.java,
@ -230,7 +230,7 @@ class CustomOkHttpNetworkFetcherUnitTest {
val method: Method = val method: Method =
CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod( CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod(
"onFetchResponse", "onFetchResponse",
CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState::class.java, OkHttpNetworkFetchState::class.java,
Call::class.java, Call::class.java,
Response::class.java, Response::class.java,
NetworkFetcher.Callback::class.java, NetworkFetcher.Callback::class.java,

View file

@ -132,7 +132,7 @@ class MediaDetailFragmentUnitTests {
private lateinit var button: Button private lateinit var button: Button
@Mock @Mock
private lateinit var detailProvider: MediaDetailPagerFragment.MediaDetailProvider private lateinit var detailProvider: MediaDetailProvider
@Mock @Mock
private lateinit var applicationKvStore: JsonKvStore private lateinit var applicationKvStore: JsonKvStore