From 8fc7e1039be5d271340ea4fa6d7351af18d8dab8 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Fri, 11 Jul 2025 21:11:20 -0500 Subject: [PATCH] Convert media package to kotlin (#6369) * 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 --- .../bookmarks/BookmarkListRootFragment.java | 3 +- .../category/CategoryDetailsActivity.kt | 4 +- .../contributions/ContributionsFragment.kt | 3 +- .../explore/ExploreListRootFragment.java | 4 +- .../explore/ExploreMapRootFragment.java | 4 +- .../nrw/commons/explore/SearchActivity.java | 9 +- .../WikidataItemDetailsActivity.java | 8 +- .../explore/media/PageableMediaFragment.kt | 2 +- .../fr/free/nrw/commons/media/Caption.java | 43 -- .../java/fr/free/nrw/commons/media/Caption.kt | 19 + .../commons/media/CaptionListViewAdapter.java | 67 -- .../commons/media/CaptionListViewAdapter.kt | 34 + .../commons/media/CommonsWikibaseItem.java | 76 -- .../media/CustomOkHttpNetworkFetcher.java | 236 ------ .../media/CustomOkHttpNetworkFetcher.kt | 199 +++++ .../nrw/commons/media/MediaDetailAdapter.kt | 76 ++ .../nrw/commons/media/MediaDetailFragment.kt | 2 - .../media/MediaDetailPagerFragment.java | 678 ------------------ .../commons/media/MediaDetailPagerFragment.kt | 622 ++++++++++++++++ .../nrw/commons/media/MediaDetailProvider.kt | 14 + .../nrw/commons/media/MwParseResponse.java | 25 - .../free/nrw/commons/media/MwParseResponse.kt | 17 + .../free/nrw/commons/media/MwParseResult.java | 18 - .../free/nrw/commons/media/MwParseResult.kt | 18 + .../nearby/fragments/NearbyParentFragment.kt | 3 +- .../BookmarkListRootFragmentUnitTest.kt | 4 +- .../CategoryDetailsActivityUnitTests.kt | 2 +- .../ContributionsFragmentUnitTests.kt | 2 +- .../ExploreListRootFragmentUnitTest.kt | 6 +- .../WikidataItemDetailsActivityUnitTests.kt | 2 +- .../explore/search/SearchActivityUnitTests.kt | 4 +- .../CustomOkHttpNetworkFetcherUnitTest.kt | 8 +- .../media/MediaDetailFragmentUnitTests.kt | 2 +- 33 files changed, 1030 insertions(+), 1184 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/media/Caption.java create mode 100644 app/src/main/java/fr/free/nrw/commons/media/Caption.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/media/CaptionListViewAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/media/CaptionListViewAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/media/CommonsWikibaseItem.java delete mode 100644 app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.java create mode 100644 app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/media/MediaDetailAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt create mode 100644 app/src/main/java/fr/free/nrw/commons/media/MediaDetailProvider.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.java create mode 100644 app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/media/MwParseResult.java create mode 100644 app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java index ca7dd3f3b..e14cbbb6f 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkListRootFragment.java @@ -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; diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.kt index 8517db744..c998f96ac 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.kt @@ -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 diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.kt index 541cc6e56..537d805c5 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.kt @@ -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 diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java index f3948caa7..e3ad90119 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreListRootFragment.java @@ -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; diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java index abf02758d..31a8e11ba 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreMapRootFragment.java @@ -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; diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java index 1651c720c..b27ffc338 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java @@ -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; diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java index e4f7ce465..ec5ea42a4 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivity.java @@ -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; diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt index e19b1b056..e7895f683 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt @@ -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 : diff --git a/app/src/main/java/fr/free/nrw/commons/media/Caption.java b/app/src/main/java/fr/free/nrw/commons/media/Caption.java deleted file mode 100644 index 2cb3b86e1..000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/Caption.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/Caption.kt b/app/src/main/java/fr/free/nrw/commons/media/Caption.kt new file mode 100644 index 000000000..ef2dadc26 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/Caption.kt @@ -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 + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/CaptionListViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/media/CaptionListViewAdapter.java deleted file mode 100644 index 759eb2af7..000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/CaptionListViewAdapter.java +++ /dev/null @@ -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 captions; - - public CaptionListViewAdapter(final List 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; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/CaptionListViewAdapter.kt b/app/src/main/java/fr/free/nrw/commons/media/CaptionListViewAdapter.kt new file mode 100644 index 000000000..05e75a23b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/CaptionListViewAdapter.kt @@ -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) : 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(R.id.caption_language_textview) + val captionTextView = captionLayout.findViewById(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 + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/CommonsWikibaseItem.java b/app/src/main/java/fr/free/nrw/commons/media/CommonsWikibaseItem.java deleted file mode 100644 index af26f0c72..000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/CommonsWikibaseItem.java +++ /dev/null @@ -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 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 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 getLabels() { - return labels; - } - - /** - * Contains the Depicts item - */ - @SerializedName("statements") - public Object getStatements() { - return statements; - } - - -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.java b/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.java deleted file mode 100644 index bbb3b73a8..000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.java +++ /dev/null @@ -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 { - - 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 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 getExtraMap(final OkHttpNetworkFetchState fetchState, - final int byteSize) { - final Map 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. - * - *

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 consumer, final ProducerContext producerContext) { - super(consumer, producerContext); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt b/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt new file mode 100644 index 000000000..c8de4022b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcher.kt @@ -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() { + + 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, 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?, 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" + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailAdapter.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailAdapter.kt new file mode 100644 index 000000000..ccc176154 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailAdapter.kt @@ -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) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt index 5980e1fb5..07574067c 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt @@ -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 diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java deleted file mode 100644 index 324d5867b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ /dev/null @@ -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 removedItems=new ArrayList(); - - public void clearRemoved(){ - removedItems.clear(); - } - public ArrayList 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 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); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt new file mode 100644 index 000000000..b66c888aa --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.kt @@ -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 = 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, 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 + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailProvider.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailProvider.kt new file mode 100644 index 000000000..591adfe75 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailProvider.kt @@ -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) +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.java b/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.java deleted file mode 100644 index 28df3811a..000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.kt b/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.kt new file mode 100644 index 000000000..fc0282a9e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/MwParseResponse.kt @@ -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 + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.java b/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.java deleted file mode 100644 index edb7ff447..000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.java +++ /dev/null @@ -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; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt b/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt new file mode 100644 index 000000000..7aacdea09 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt @@ -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 + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt index a0dcead07..5c991f465 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt @@ -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 { diff --git a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarkListRootFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarkListRootFragmentUnitTest.kt index 5098bd0c1..d168fe6e4 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarkListRootFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/bookmarks/BookmarkListRootFragmentUnitTest.kt @@ -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 diff --git a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDetailsActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDetailsActivityUnitTests.kt index fd6bc7976..b1f40a08f 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDetailsActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/category/CategoryDetailsActivityUnitTests.kt @@ -76,7 +76,7 @@ class CategoryDetailsActivityUnitTests { @Test @Throws(Exception::class) fun testGetTotalMediaCount() { - activity.totalMediaCount + activity.getTotalMediaCount() } @Test diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsFragmentUnitTests.kt index e3f1c86cc..16848f7dd 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsFragmentUnitTests.kt @@ -334,7 +334,7 @@ class ContributionsFragmentUnitTests { @Throws(Exception::class) fun testGetTotalMediaCount() { Shadows.shadowOf(Looper.getMainLooper()).idle() - fragment.totalMediaCount + fragment.getTotalMediaCount() } @Test diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/ExploreListRootFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/ExploreListRootFragmentUnitTest.kt index 9dc94293f..62068f4f9 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/ExploreListRootFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/ExploreListRootFragmentUnitTest.kt @@ -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 diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivityUnitTests.kt index bf5aca6e2..8ba2e86a8 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/depictions/WikidataItemDetailsActivityUnitTests.kt @@ -108,7 +108,7 @@ class WikidataItemDetailsActivityUnitTests { @Test @Throws(Exception::class) fun testGetTotalMediaCount() { - activity.totalMediaCount + activity.getTotalMediaCount() } @Test diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/search/SearchActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/search/SearchActivityUnitTests.kt index 751046e7f..00b9c0fd9 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/search/SearchActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/search/SearchActivityUnitTests.kt @@ -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 diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcherUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcherUnitTest.kt index 16a35a67b..225b4bd80 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcherUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/media/CustomOkHttpNetworkFetcherUnitTest.kt @@ -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, diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt index b6d3c6e28..6159a3ccf 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt @@ -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