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..d34c162dc 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
@@ -322,12 +320,6 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
binding.seeMore.setUnderlinedText(R.string.nominated_see_more)
- if (isCategoryImage) {
- binding.authorLinearLayout.visibility = View.VISIBLE
- } else {
- binding.authorLinearLayout.visibility = View.GONE
- }
-
if (!sessionManager.isUserLoggedIn) {
binding.categoryEditButton.visibility = View.GONE
binding.descriptionEdit.visibility = View.GONE
@@ -816,10 +808,27 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
categoryNames.clear()
categoryNames.addAll(media.categories!!)
- if (media.author == null || media.author == "") {
- binding.authorLinearLayout.visibility = View.GONE
- } else {
- binding.mediaDetailAuthor.text = media.author
+ // Show author or uploader information for licensing compliance
+ val authorName = media.getAttributedAuthor()
+ val uploaderName = media.user
+
+ when {
+ !authorName.isNullOrEmpty() -> {
+ // Show author if available
+ binding.mediaDetailAuthorLabel.text = getString(R.string.media_detail_author)
+ binding.mediaDetailAuthor.text = authorName
+ binding.authorLinearLayout.visibility = View.VISIBLE
+ }
+ !uploaderName.isNullOrEmpty() -> {
+ // Show uploader as fallback
+ binding.mediaDetailAuthorLabel.text = getString(R.string.media_detail_uploader)
+ binding.mediaDetailAuthor.text = uploaderName
+ binding.authorLinearLayout.visibility = View.VISIBLE
+ }
+ else -> {
+ // Hide if neither author nor uploader is available
+ binding.authorLinearLayout.visibility = View.GONE
+ }
}
}
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/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt
index c39d8901d..6fc2d0795 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt
@@ -7,6 +7,7 @@ class NearbyResultItem(
private val wikipediaArticle: ResultTuple?,
private val commonsArticle: ResultTuple?,
private val location: ResultTuple?,
+ @field:SerializedName("itemLabel")
private val label: ResultTuple?,
@field:SerializedName("streetAddress") private val address: ResultTuple?,
private val icon: ResultTuple?,
@@ -15,7 +16,7 @@ class NearbyResultItem(
@field:SerializedName("commonsCategory") private val commonsCategory: ResultTuple?,
@field:SerializedName("pic") private val pic: ResultTuple?,
@field:SerializedName("destroyed") private val destroyed: ResultTuple?,
- @field:SerializedName("description") private val description: ResultTuple?,
+ @field:SerializedName("itemDescription") private val description: ResultTuple?,
@field:SerializedName("endTime") private val endTime: ResultTuple?,
@field:SerializedName("monument") private val monument: ResultTuple?,
@field:SerializedName("dateOfOfficialClosure") private val dateOfOfficialClosure: ResultTuple?,
diff --git a/app/src/main/res/layout/fragment_media_detail.xml b/app/src/main/res/layout/fragment_media_detail.xml
index 7ce90d19e..52afe3fc3 100644
--- a/app/src/main/res/layout/fragment_media_detail.xml
+++ b/app/src/main/res/layout/fragment_media_detail.xml
@@ -125,27 +125,6 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
Ахь шинафакторийн аутентификацин код йазо йеза