Convert MediaDetailPagerFragment to kotlin

This commit is contained in:
Paul Hawke 2025-07-11 15:13:16 -05:00
parent 1d1210e863
commit 48658a693c
3 changed files with 625 additions and 592 deletions

View file

@ -8,7 +8,7 @@ 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)
internal class MediaDetailAdapter(
class MediaDetailAdapter(
val mediaDetailPagerFragment: MediaDetailPagerFragment,
fm: FragmentManager
) : FragmentStatePagerAdapter(fm) {
@ -24,7 +24,7 @@ internal class MediaDetailAdapter(
Timber.d("Skipping getItem. Returning as activity is destroyed!")
return Fragment()
}
mediaDetailPagerFragment.binding.mediaDetailsPager.postDelayed(
mediaDetailPagerFragment.binding!!.mediaDetailsPager.postDelayed(
{ mediaDetailPagerFragment.requireActivity().invalidateOptionsMenu() }, 5
)
}
@ -48,7 +48,7 @@ internal class MediaDetailAdapter(
Timber.d("Skipping getCount. Returning as activity is destroyed!")
return 0
}
return mediaDetailPagerFragment.provider.getTotalMediaCount()
return mediaDetailPagerFragment.mediaDetailProvider!!.getTotalMediaCount()
}
/**

View file

@ -1,589 +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.core.content.ContextCompat;
import androidx.fragment.app.FragmentManager;
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();
FragmentMediaDetailPagerBinding binding;
boolean editable;
boolean isFeaturedImage;
boolean isWikipediaButtonDisplayed;
MediaDetailAdapter adapter;
Bookmark bookmark;
MediaDetailProvider provider;
boolean isFromFeaturedRootFragment;
int position;
/**
* ProgressBar used to indicate the loading status of media items.
*/
ProgressBar imageProgressBar;
ArrayList<Integer> removedItems=new ArrayList<Integer>();
public void clearRemoved(){
removedItems.clear();
}
public ArrayList<Integer> getRemovedItems() {
return removedItems;
}
/**
* Use this factory method to create a new instance of this fragment using the provided
* parameters.
*
* This method will create a new instance of MediaDetailPagerFragment and the arguments will be
* saved to a bundle which will be later available in the {@link #onCreate(Bundle)}
* @param editable
* @param isFeaturedImage
* @return
*/
public static MediaDetailPagerFragment newInstance(boolean editable, boolean isFeaturedImage) {
MediaDetailPagerFragment mediaDetailPagerFragment = new MediaDetailPagerFragment();
Bundle args = new Bundle();
args.putBoolean("is_editable", editable);
args.putBoolean("is_featured_image", isFeaturedImage);
mediaDetailPagerFragment.setArguments(args);
return mediaDetailPagerFragment;
}
public MediaDetailPagerFragment() {
// Required empty public constructor
};
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentMediaDetailPagerBinding.inflate(inflater, container, false);
binding.mediaDetailsPager.addOnPageChangeListener(this);
// Initialize the ProgressBar by finding it in the layout
imageProgressBar = binding.getRoot().findViewById(R.id.itemProgressBar);
adapter = new MediaDetailAdapter(this, getChildFragmentManager());
// ActionBar is now supported in both activities - if this crashes something is quite wrong
final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
else {
throw new AssertionError("Action bar should not be null!");
}
// If fragment is associated with ProfileActivity, then hide the tabLayout
if (getActivity() instanceof ProfileActivity) {
((ProfileActivity)getActivity()).setTabLayoutVisibility(false);
}
// Else if fragment is associated with MainActivity then hide that tab layout
else if (getActivity() instanceof MainActivity) {
((MainActivity)getActivity()).hideTabs();
}
binding.mediaDetailsPager.setAdapter(adapter);
if (savedInstanceState != null) {
final int pageNumber = savedInstanceState.getInt("current-page");
binding.mediaDetailsPager.setCurrentItem(pageNumber, false);
getActivity().invalidateOptionsMenu();
}
adapter.notifyDataSetChanged();
return binding.getRoot();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt("current-page", binding.mediaDetailsPager.getCurrentItem());
outState.putBoolean("editable", editable);
outState.putBoolean("isFeaturedImage", isFeaturedImage);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable", false);
isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage", false);
}
setHasOptionsMenu(true);
initProvider();
}
/**
* initialise the provider, based on from where the fragment was started, as in from an activity
* or a fragment
*/
private void initProvider() {
if (getParentFragment() instanceof MediaDetailProvider) {
provider = (MediaDetailProvider) getParentFragment();
} else if (getActivity() instanceof MediaDetailProvider) {
provider = (MediaDetailProvider) getActivity();
} else {
throw new ClassCastException("Parent must implement MediaDetailProvider");
}
}
public MediaDetailProvider getMediaDetailProvider() {
return provider;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (getActivity() == null) {
Timber.d("Returning as activity is destroyed!");
return true;
}
Media m = provider.getMediaAtPosition(binding.mediaDetailsPager.getCurrentItem());
MediaDetailFragment mediaDetailFragment = this.adapter.getCurrentMediaDetailFragment();
switch (item.getItemId()) {
case R.id.menu_bookmark_current_image:
boolean bookmarkExists = bookmarkDao.updateBookmark(bookmark);
Snackbar snackbar = bookmarkExists ? Snackbar.make(getView(), R.string.add_bookmark, Snackbar.LENGTH_LONG) : Snackbar.make(getView(), R.string.remove_bookmark, Snackbar.LENGTH_LONG);
snackbar.show();
updateBookmarkState(item);
return true;
case R.id.menu_copy_link:
String uri = m.getPageTitle().getCanonicalUri();
ClipboardUtils.copy("shareLink", uri, requireContext());
Timber.d("Copied share link to clipboard: %s", uri);
Toast.makeText(requireContext(), getString(R.string.menu_link_copied),
Toast.LENGTH_SHORT).show();
return true;
case R.id.menu_share_current_image:
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_TEXT, m.getDisplayTitle() + " \n" + m.getPageTitle().getCanonicalUri());
startActivity(Intent.createChooser(shareIntent, "Share image via..."));
//Add media detail to backstack when the share button is clicked
//So that when the share is cancelled or completed the media detail page is on top
// of back stack fixing:https://github.com/commons-app/apps-android-commons/issues/2296
FragmentManager supportFragmentManager = getActivity().getSupportFragmentManager();
if (supportFragmentManager.getBackStackEntryCount() < 2) {
supportFragmentManager
.beginTransaction()
.addToBackStack(MediaDetailPagerFragment.class.getName())
.commit();
supportFragmentManager.executePendingTransactions();
}
return true;
case R.id.menu_browser_current_image:
// View in browser
handleWebUrl(requireContext(), Uri.parse(m.getPageTitle().getMobileUri()));
return true;
case R.id.menu_download_current_image:
// Download
if (!NetworkUtils.isInternetConnectionEstablished(getActivity())) {
ViewUtil.showShortSnackbar(getView(), R.string.no_internet);
return false;
}
DownloadUtils.downloadMedia(getActivity(), m);
return true;
case R.id.menu_set_as_wallpaper:
// Set wallpaper
setWallpaper(m);
return true;
case R.id.menu_set_as_avatar:
// Set avatar
setAvatar(m);
return true;
case R.id.menu_view_user_page:
if (m != null && m.getUser() != null) {
ProfileActivity.startYourself(getActivity(), m.getUser(),
!Objects.equals(sessionManager.getUserName(), m.getUser()));
}
return true;
case R.id.menu_view_report:
showReportDialog(m);
case R.id.menu_view_set_white_background:
if (mediaDetailFragment != null) {
mediaDetailFragment.onImageBackgroundChanged(ContextCompat.getColor(getContext(), R.color.white));
}
return true;
case R.id.menu_view_set_black_background:
if (mediaDetailFragment != null) {
mediaDetailFragment.onImageBackgroundChanged(ContextCompat.getColor(getContext(), R.color.black));
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void showReportDialog(final Media media) {
if (media == null) {
return;
}
final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
final String[] values = requireContext().getResources()
.getStringArray(R.array.report_violation_options);
builder.setTitle(R.string.report_violation);
builder.setItems(R.array.report_violation_options, (dialog, which) -> {
sendReportEmail(media, values[which]);
});
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {});
builder.setCancelable(false);
builder.show();
}
private void sendReportEmail(final Media media, final String type) {
final String technicalInfo = getTechInfo(media, type);
final Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO);
feedbackIntent.setType("message/rfc822");
feedbackIntent.setData(Uri.parse("mailto:"));
feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
new String[]{CommonsApplication.REPORT_EMAIL});
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
CommonsApplication.REPORT_EMAIL_SUBJECT);
feedbackIntent.putExtra(Intent.EXTRA_TEXT, technicalInfo);
try {
startActivity(feedbackIntent);
} catch (final ActivityNotFoundException e) {
Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show();
}
}
private String getTechInfo(final Media media, final String type) {
final StringBuilder builder = new StringBuilder();
builder.append("Report type: ")
.append(type)
.append("\n\n");
builder.append("Image that you want to report: ")
.append(media.getImageUrl())
.append("\n\n");
builder.append("User that you want to report: ")
.append(media.getUser())
.append("\n\n");
if (sessionManager.getUserName() != null) {
builder.append("Your username: ")
.append(sessionManager.getUserName())
.append("\n\n");
}
builder.append("Violation reason: ")
.append("\n");
builder.append("----------------------------------------------")
.append("\n")
.append("(please write reason here)")
.append("\n")
.append("----------------------------------------------")
.append("\n\n")
.append("Thank you for your report! Our team will investigate as soon as possible.")
.append("\n")
.append("Please note that images also have a `Nominate for deletion` button.");
return builder.toString();
}
/**
* Set the media as the device's wallpaper if the imageUrl is not null
* Fails silently if setting the wallpaper fails
* @param media
*/
private void setWallpaper(Media media) {
if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
Timber.d("Media URL not present");
return;
}
ImageUtils.setWallpaperFromImageUrl(getActivity(), Uri.parse(media.getImageUrl()));
}
/**
* Set the media as user's leaderboard avatar
* @param media
*/
private void setAvatar(Media media) {
if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
Timber.d("Media URL not present");
return;
}
ImageUtils.setAvatarFromImageUrl(getActivity(), media.getImageUrl(),
Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
okHttpJsonApiClient, compositeDisposable);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (!editable) { // Disable menu options for editable views
menu.clear(); // see http://stackoverflow.com/a/8495697/17865
inflater.inflate(R.menu.fragment_image_detail, menu);
if (binding.mediaDetailsPager != null) {
MediaDetailProvider provider = getMediaDetailProvider();
if(provider == null) {
return;
}
final int position;
if (isFromFeaturedRootFragment) {
position = this.position;
} else {
position = binding.mediaDetailsPager.getCurrentItem();
}
Media m = provider.getMediaAtPosition(position);
if (m != null) {
// Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_copy_link).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_download_current_image).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true).setVisible(true);
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(true).setVisible(true);
if (m.getUser() != null) {
menu.findItem(R.id.menu_view_user_page).setEnabled(true).setVisible(true);
}
try {
URL mediaUrl = new URL(m.getImageUrl());
this.handleBackgroundColorMenuItems(
() -> BitmapFactory.decodeStream(mediaUrl.openConnection().getInputStream()),
menu
);
} catch (Exception e) {
Timber.e("Cant detect media transparency");
}
// Initialize bookmark object
bookmark = new Bookmark(
m.getFilename(),
m.getAuthorOrUser(),
BookmarkPicturesContentProvider.uriForName(m.getFilename())
);
updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image));
final Integer contributionState = provider.getContributionStateAt(position);
if (contributionState != null) {
switch (contributionState) {
case Contribution.STATE_FAILED:
case Contribution.STATE_IN_PROGRESS:
case Contribution.STATE_QUEUED:
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
.setVisible(false);
menu.findItem(R.id.menu_copy_link).setEnabled(false)
.setVisible(false);
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
.setVisible(false);
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
.setVisible(false);
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
.setVisible(false);
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
.setVisible(false);
break;
case Contribution.STATE_COMPLETED:
// Default set of menu items works fine. Treat same as regular media object
break;
}
}
} else {
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
.setVisible(false);
menu.findItem(R.id.menu_copy_link).setEnabled(false)
.setVisible(false);
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
.setVisible(false);
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
.setVisible(false);
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
.setVisible(false);
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
.setVisible(false);
}
if (!sessionManager.isUserLoggedIn()) {
menu.findItem(R.id.menu_set_as_avatar).setVisible(false);
}
}
}
}
/**
* Decide wether or not we should display the background color menu items
* We display them if the image is transparent
* @param getBitmap
* @param menu
*/
private void handleBackgroundColorMenuItems(Callable<Bitmap> getBitmap, Menu menu) {
Observable.fromCallable(
getBitmap
).subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(image -> {
if (image.hasAlpha()) {
menu.findItem(R.id.menu_view_set_white_background).setVisible(true).setEnabled(true);
menu.findItem(R.id.menu_view_set_black_background).setVisible(true).setEnabled(true);
}
});
}
private void updateBookmarkState(MenuItem item) {
boolean isBookmarked = bookmarkDao.findBookmark(bookmark);
if(isBookmarked) {
if(removedItems.contains(binding.mediaDetailsPager.getCurrentItem())) {
removedItems.remove(new Integer(binding.mediaDetailsPager.getCurrentItem()));
}
}
else {
if(!removedItems.contains(binding.mediaDetailsPager.getCurrentItem())) {
removedItems.add(binding.mediaDetailsPager.getCurrentItem());
}
}
int icon = isBookmarked ? R.drawable.menu_ic_round_star_filled_24px : R.drawable.menu_ic_round_star_border_24px;
item.setIcon(icon);
}
public void showImage(int i, boolean isWikipediaButtonDisplayed) {
this.isWikipediaButtonDisplayed = isWikipediaButtonDisplayed;
setViewPagerCurrentItem(i);
}
public void showImage(int i) {
setViewPagerCurrentItem(i);
}
/**
* This function waits for the item to load then sets the item to current item
* @param position current item that to be shown
*/
private void setViewPagerCurrentItem(int position) {
final Handler handler = new Handler(Looper.getMainLooper());
final Runnable runnable = new Runnable() {
@Override
public void run() {
// Show the ProgressBar while waiting for the item to load
imageProgressBar.setVisibility(View.VISIBLE);
// Check if the adapter has enough items loaded
if(adapter.getCount() > position){
// Set the current item in the ViewPager
binding.mediaDetailsPager.setCurrentItem(position, false);
// Hide the ProgressBar once the item is loaded
imageProgressBar.setVisibility(View.GONE);
} else {
// If the item is not ready yet, post the Runnable again
handler.post(this);
}
}
};
// Start the Runnable
handler.post(runnable);
}
/**
* The method notify the viewpager that number of items have changed.
*/
public void notifyDataSetChanged(){
if (null != adapter) {
adapter.notifyDataSetChanged();
}
}
@Override
public void onPageScrolled(int i, float v, int i2) {
if(getActivity() == null) {
Timber.d("Returning as activity is destroyed!");
return;
}
getActivity().invalidateOptionsMenu();
}
@Override
public void onPageSelected(int i) {
}
@Override
public void onPageScrollStateChanged(int i) {
}
public void onDataSetChanged() {
if (null != adapter) {
adapter.notifyDataSetChanged();
}
}
/**
* Called after the media is nominated for deletion
*
* @param index item position that has been nominated
*/
@Override
public void nominatingForDeletion(int index) {
provider.refreshNominatedMedia(index);
}
}

View file

@ -0,0 +1,622 @@
package fr.free.nrw.commons.media
import android.content.ActivityNotFoundException
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.viewpager.widget.ViewPager.OnPageChangeListener
import com.google.android.material.snackbar.Snackbar
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.bookmarks.models.Bookmark
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.contributions.MainActivity
import fr.free.nrw.commons.databinding.FragmentMediaDetailPagerBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.profile.ProfileActivity
import fr.free.nrw.commons.profile.ProfileActivity.Companion.startYourself
import fr.free.nrw.commons.utils.ClipboardUtils.copy
import fr.free.nrw.commons.utils.DownloadUtils.downloadMedia
import fr.free.nrw.commons.utils.ImageUtils.setAvatarFromImageUrl
import fr.free.nrw.commons.utils.ImageUtils.setWallpaperFromImageUrl
import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
import fr.free.nrw.commons.utils.ViewUtil.showShortSnackbar
import fr.free.nrw.commons.utils.handleWebUrl
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.functions.Consumer
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import java.net.URL
import java.util.concurrent.Callable
import javax.inject.Inject
import androidx.core.net.toUri
class MediaDetailPagerFragment : CommonsDaggerSupportFragment(), OnPageChangeListener,
MediaDetailFragment.Callback {
@JvmField
@Inject
var bookmarkDao: BookmarkPicturesDao? = null
@JvmField
@Inject
var okHttpJsonApiClient: OkHttpJsonApiClient? = null
@JvmField
@Inject
var sessionManager: SessionManager? = null
var binding: FragmentMediaDetailPagerBinding? = null
var editable: Boolean = false
var isFeaturedImage: Boolean = false
var isWikipediaButtonDisplayed: Boolean = false
var adapter: MediaDetailAdapter? = null
var bookmark: Bookmark? = null
var mediaDetailProvider: MediaDetailProvider? = null
var isFromFeaturedRootFragment: Boolean = false
var position: Int = 0
/**
* ProgressBar used to indicate the loading status of media items.
*/
var imageProgressBar: ProgressBar? = null
var removedItems: ArrayList<Int> = ArrayList()
fun clearRemoved() = removedItems.clear()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentMediaDetailPagerBinding.inflate(inflater, container, false)
binding!!.mediaDetailsPager.addOnPageChangeListener(this)
// Initialize the ProgressBar by finding it in the layout
imageProgressBar = binding!!.root.findViewById(R.id.itemProgressBar)
adapter = MediaDetailAdapter(this, childFragmentManager)
// ActionBar is now supported in both activities - if this crashes something is quite wrong
val actionBar = (activity as AppCompatActivity).supportActionBar
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true)
} else {
throw AssertionError("Action bar should not be null!")
}
// If fragment is associated with ProfileActivity, then hide the tabLayout
if (activity is ProfileActivity) {
(activity as ProfileActivity).setTabLayoutVisibility(false)
} else if (activity is MainActivity) {
(activity as MainActivity).hideTabs()
}
binding!!.mediaDetailsPager.adapter = adapter
if (savedInstanceState != null) {
val pageNumber = savedInstanceState.getInt("current-page")
binding!!.mediaDetailsPager.setCurrentItem(pageNumber, false)
requireActivity().invalidateOptionsMenu()
}
adapter!!.notifyDataSetChanged()
return binding!!.root
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("current-page", binding!!.mediaDetailsPager.currentItem)
outState.putBoolean("editable", editable)
outState.putBoolean("isFeaturedImage", isFeaturedImage)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
editable = savedInstanceState.getBoolean("editable", false)
isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage", false)
}
setHasOptionsMenu(true)
initProvider()
}
/**
* initialise the provider, based on from where the fragment was started, as in from an activity
* or a fragment
*/
private fun initProvider() {
if (parentFragment is MediaDetailProvider) {
mediaDetailProvider = parentFragment as MediaDetailProvider
} else if (activity is MediaDetailProvider) {
mediaDetailProvider = activity as MediaDetailProvider?
} else {
throw ClassCastException("Parent must implement MediaDetailProvider")
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (activity == null) {
Timber.d("Returning as activity is destroyed!")
return true
}
val m = mediaDetailProvider!!.getMediaAtPosition(binding!!.mediaDetailsPager.currentItem)
val mediaDetailFragment = adapter!!.currentMediaDetailFragment
when (item.itemId) {
R.id.menu_bookmark_current_image -> {
val bookmarkExists = bookmarkDao!!.updateBookmark(bookmark)
val snackbar = if (bookmarkExists) Snackbar.make(
requireView(),
R.string.add_bookmark,
Snackbar.LENGTH_LONG
) else Snackbar.make(
requireView(), R.string.remove_bookmark, Snackbar.LENGTH_LONG
)
snackbar.show()
updateBookmarkState(item)
return true
}
R.id.menu_copy_link -> {
val uri = m!!.pageTitle.canonicalUri
copy("shareLink", uri, requireContext())
Timber.d("Copied share link to clipboard: %s", uri)
Toast.makeText(
requireContext(), getString(R.string.menu_link_copied),
Toast.LENGTH_SHORT
).show()
return true
}
R.id.menu_share_current_image -> {
val shareIntent = Intent(Intent.ACTION_SEND)
shareIntent.setType("text/plain")
shareIntent.putExtra(
Intent.EXTRA_TEXT, """${m!!.displayTitle}
${m.pageTitle.canonicalUri}"""
)
startActivity(Intent.createChooser(shareIntent, "Share image via..."))
//Add media detail to backstack when the share button is clicked
//So that when the share is cancelled or completed the media detail page is on top
// of back stack fixing:https://github.com/commons-app/apps-android-commons/issues/2296
val supportFragmentManager = requireActivity().supportFragmentManager
if (supportFragmentManager.backStackEntryCount < 2) {
supportFragmentManager
.beginTransaction()
.addToBackStack(MediaDetailPagerFragment::class.java.name)
.commit()
supportFragmentManager.executePendingTransactions()
}
return true
}
R.id.menu_browser_current_image -> {
// View in browser
handleWebUrl(requireContext(), m!!.pageTitle.mobileUri.toUri())
return true
}
R.id.menu_download_current_image -> {
// Download
if (!isInternetConnectionEstablished(activity)) {
showShortSnackbar(requireView(), R.string.no_internet)
return false
}
downloadMedia(activity, m!!)
return true
}
R.id.menu_set_as_wallpaper -> {
// Set wallpaper
setWallpaper(m!!)
return true
}
R.id.menu_set_as_avatar -> {
// Set avatar
setAvatar(m!!)
return true
}
R.id.menu_view_user_page -> {
if (m?.user != null) {
startYourself(
requireActivity(), m.user!!,
sessionManager!!.userName != m.user
)
}
return true
}
R.id.menu_view_report -> {
showReportDialog(m)
mediaDetailFragment?.onImageBackgroundChanged(
ContextCompat.getColor(
requireContext(),
R.color.white
)
)
return true
}
R.id.menu_view_set_white_background -> {
mediaDetailFragment?.onImageBackgroundChanged(
ContextCompat.getColor(
requireContext(),
R.color.white
)
)
return true
}
R.id.menu_view_set_black_background -> {
mediaDetailFragment?.onImageBackgroundChanged(
ContextCompat.getColor(
requireContext(),
R.color.black
)
)
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
private fun showReportDialog(media: Media?) {
if (media == null) {
return
}
val builder = AlertDialog.Builder(requireActivity())
val values = requireContext().resources
.getStringArray(R.array.report_violation_options)
builder.setTitle(R.string.report_violation)
builder.setItems(
R.array.report_violation_options
) { dialog: DialogInterface?, which: Int ->
sendReportEmail(media, values[which])
}
builder.setNegativeButton(
R.string.cancel
) { dialog: DialogInterface?, which: Int -> }
builder.setCancelable(false)
builder.show()
}
private fun sendReportEmail(media: Media, type: String) {
val technicalInfo = getTechInfo(media, type)
val feedbackIntent = Intent(Intent.ACTION_SENDTO)
feedbackIntent.setType("message/rfc822")
feedbackIntent.setData(Uri.parse("mailto:"))
feedbackIntent.putExtra(
Intent.EXTRA_EMAIL,
arrayOf(CommonsApplication.REPORT_EMAIL)
)
feedbackIntent.putExtra(
Intent.EXTRA_SUBJECT,
CommonsApplication.REPORT_EMAIL_SUBJECT
)
feedbackIntent.putExtra(Intent.EXTRA_TEXT, technicalInfo)
try {
startActivity(feedbackIntent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(activity, R.string.no_email_client, Toast.LENGTH_SHORT).show()
}
}
private fun getTechInfo(media: Media, type: String): String {
val builder = StringBuilder()
builder.append("Report type: ")
.append(type)
.append("\n\n")
builder.append("Image that you want to report: ")
.append(media.imageUrl)
.append("\n\n")
builder.append("User that you want to report: ")
.append(media.user)
.append("\n\n")
if (sessionManager!!.userName != null) {
builder.append("Your username: ")
.append(sessionManager!!.userName)
.append("\n\n")
}
builder.append("Violation reason: ")
.append("\n")
builder.append("----------------------------------------------")
.append("\n")
.append("(please write reason here)")
.append("\n")
.append("----------------------------------------------")
.append("\n\n")
.append("Thank you for your report! Our team will investigate as soon as possible.")
.append("\n")
.append("Please note that images also have a `Nominate for deletion` button.")
return builder.toString()
}
/**
* Set the media as the device's wallpaper if the imageUrl is not null
* Fails silently if setting the wallpaper fails
* @param media
*/
private fun setWallpaper(media: Media) {
if (media.imageUrl == null || media.imageUrl!!.isEmpty()) {
Timber.d("Media URL not present")
return
}
setWallpaperFromImageUrl(requireActivity(), media.imageUrl!!.toUri())
}
/**
* Set the media as user's leaderboard avatar
* @param media
*/
private fun setAvatar(media: Media) {
if (media.imageUrl == null || media.imageUrl!!.isEmpty()) {
Timber.d("Media URL not present")
return
}
setAvatarFromImageUrl(
requireActivity(), media.imageUrl!!,
sessionManager!!.currentAccount!!.name,
okHttpJsonApiClient!!, Companion.compositeDisposable
)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
if (!editable) { // Disable menu options for editable views
menu.clear() // see http://stackoverflow.com/a/8495697/17865
inflater.inflate(R.menu.fragment_image_detail, menu)
if (binding!!.mediaDetailsPager != null) {
val provider = mediaDetailProvider ?: return
val position = if (isFromFeaturedRootFragment) {
position
} else {
binding!!.mediaDetailsPager.currentItem
}
val m = provider.getMediaAtPosition(position)
if (m != null) {
// Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true)
menu.findItem(R.id.menu_copy_link).setEnabled(true).setVisible(true)
menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true)
menu.findItem(R.id.menu_download_current_image).setEnabled(true)
.setVisible(true)
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true)
.setVisible(true)
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(true).setVisible(true)
if (m.user != null) {
menu.findItem(R.id.menu_view_user_page).setEnabled(true).setVisible(true)
}
try {
val mediaUrl = URL(m.imageUrl)
handleBackgroundColorMenuItems({
BitmapFactory.decodeStream(
mediaUrl.openConnection().getInputStream()
)
}, menu)
} catch (e: Exception) {
Timber.e("Cant detect media transparency")
}
// Initialize bookmark object
bookmark = Bookmark(
m.filename,
m.getAuthorOrUser(),
BookmarkPicturesContentProvider.uriForName(m.filename)
)
updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image))
val contributionState = provider.getContributionStateAt(position)
if (contributionState != null) {
when (contributionState) {
Contribution.STATE_FAILED, Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED -> {
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
.setVisible(false)
menu.findItem(R.id.menu_copy_link).setEnabled(false)
.setVisible(false)
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
.setVisible(false)
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
.setVisible(false)
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
.setVisible(false)
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
.setVisible(false)
}
Contribution.STATE_COMPLETED -> {}
}
}
} else {
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
.setVisible(false)
menu.findItem(R.id.menu_copy_link).setEnabled(false)
.setVisible(false)
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
.setVisible(false)
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
.setVisible(false)
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
.setVisible(false)
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
.setVisible(false)
}
if (!sessionManager!!.isUserLoggedIn) {
menu.findItem(R.id.menu_set_as_avatar).setVisible(false)
}
}
}
}
/**
* Decide wether or not we should display the background color menu items
* We display them if the image is transparent
* @param getBitmap
* @param menu
*/
private fun handleBackgroundColorMenuItems(getBitmap: Callable<Bitmap>, menu: Menu) {
Observable.fromCallable(
getBitmap
).subscribeOn(Schedulers.newThread())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(Consumer { image: Bitmap ->
if (image.hasAlpha()) {
menu.findItem(R.id.menu_view_set_white_background).setVisible(true)
.setEnabled(true)
menu.findItem(R.id.menu_view_set_black_background).setVisible(true)
.setEnabled(true)
}
})
}
private fun updateBookmarkState(item: MenuItem) {
val isBookmarked = bookmarkDao!!.findBookmark(bookmark)
if (isBookmarked) {
if (removedItems.contains(binding!!.mediaDetailsPager.currentItem)) {
removedItems.remove(binding!!.mediaDetailsPager.currentItem)
}
} else {
if (!removedItems.contains(binding!!.mediaDetailsPager.currentItem)) {
removedItems.add(binding!!.mediaDetailsPager.currentItem)
}
}
item.setIcon(if (isBookmarked) {
R.drawable.menu_ic_round_star_filled_24px
} else {
R.drawable.menu_ic_round_star_border_24px
})
}
fun showImage(i: Int, isWikipediaButtonDisplayed: Boolean) {
this.isWikipediaButtonDisplayed = isWikipediaButtonDisplayed
setViewPagerCurrentItem(i)
}
fun showImage(i: Int) {
setViewPagerCurrentItem(i)
}
/**
* This function waits for the item to load then sets the item to current item
* @param position current item that to be shown
*/
private fun setViewPagerCurrentItem(position: Int) {
val handler = Handler(Looper.getMainLooper())
val runnable: Runnable = object : Runnable {
override fun run() {
// Show the ProgressBar while waiting for the item to load
imageProgressBar!!.visibility = View.VISIBLE
// Check if the adapter has enough items loaded
if (adapter!!.count > position) {
// Set the current item in the ViewPager
binding!!.mediaDetailsPager.setCurrentItem(position, false)
// Hide the ProgressBar once the item is loaded
imageProgressBar!!.visibility = View.GONE
} else {
// If the item is not ready yet, post the Runnable again
handler.post(this)
}
}
}
// Start the Runnable
handler.post(runnable)
}
/**
* The method notify the viewpager that number of items have changed.
*/
fun notifyDataSetChanged() {
if (null != adapter) {
adapter!!.notifyDataSetChanged()
}
}
override fun onPageScrolled(i: Int, v: Float, i2: Int) {
if (activity == null) {
Timber.d("Returning as activity is destroyed!")
return
}
requireActivity().invalidateOptionsMenu()
}
override fun onPageSelected(i: Int) {
}
override fun onPageScrollStateChanged(i: Int) {
}
fun onDataSetChanged() {
if (null != adapter) {
adapter!!.notifyDataSetChanged()
}
}
/**
* Called after the media is nominated for deletion
*
* @param index item position that has been nominated
*/
override fun nominatingForDeletion(index: Int) {
mediaDetailProvider!!.refreshNominatedMedia(index)
}
companion object {
private val compositeDisposable = CompositeDisposable()
/**
* Use this factory method to create a new instance of this fragment using the provided
* parameters.
*
* This method will create a new instance of MediaDetailPagerFragment and the arguments will be
* saved to a bundle which will be later available in the [.onCreate]
* @param editable
* @param isFeaturedImage
* @return
*/
@JvmStatic
fun newInstance(editable: Boolean, isFeaturedImage: Boolean): MediaDetailPagerFragment {
val mediaDetailPagerFragment = MediaDetailPagerFragment()
val args = Bundle()
args.putBoolean("is_editable", editable)
args.putBoolean("is_featured_image", isFeaturedImage)
mediaDetailPagerFragment.arguments = args
return mediaDetailPagerFragment
}
}
}