package org.wikipedia.gallery;
import android.app.Activity;
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.ColorInt;
import android.support.annotation.ColorRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.wikipedia.R;
import org.wikipedia.WikipediaApp;
import org.wikipedia.activity.ThemedActionBarActivity;
import org.wikipedia.analytics.GalleryFunnel;
import org.wikipedia.concurrency.CallbackTask;
import org.wikipedia.dataclient.WikiSite;
import org.wikipedia.feed.image.FeaturedImage;
import org.wikipedia.history.HistoryEntry;
import org.wikipedia.json.GsonMarshaller;
import org.wikipedia.json.GsonUnmarshaller;
import org.wikipedia.page.ExclusiveBottomSheetPresenter;
import org.wikipedia.page.LinkMovementMethodExt;
import org.wikipedia.page.PageActivity;
import org.wikipedia.page.PageTitle;
import org.wikipedia.page.linkpreview.LinkPreviewDialog;
import org.wikipedia.readinglist.AddToReadingListDialog;
import org.wikipedia.theme.Theme;
import org.wikipedia.util.ClipboardUtil;
import org.wikipedia.util.FeedbackUtil;
import org.wikipedia.util.GradientUtil;
import org.wikipedia.util.ShareUtil;
import org.wikipedia.util.StringUtil;
import org.wikipedia.util.log.L;
import org.wikipedia.views.ViewAnimations;
import org.wikipedia.views.ViewUtil;
import org.wikipedia.views.WikiErrorView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.wikipedia.util.StringUtil.strip;
import static org.wikipedia.util.UriUtil.handleExternalLink;
import static org.wikipedia.util.UriUtil.resolveProtocolRelativeUrl;
public class GalleryActivity extends ThemedActionBarActivity implements LinkPreviewDialog.Callback {
public static final int ACTIVITY_RESULT_PAGE_SELECTED = 1;
public static final String EXTRA_PAGETITLE = "pageTitle";
public static final String EXTRA_FILENAME = "filename";
public static final String EXTRA_WIKI = "wiki";
public static final String EXTRA_SOURCE = "source";
public static final String EXTRA_FEATURED_IMAGE = "featuredImage";
public static final String EXTRA_FEATURED_IMAGE_AGE = "featuredImageAge";
@NonNull private WikipediaApp app = WikipediaApp.getInstance();
@NonNull private ExclusiveBottomSheetPresenter bottomSheetPresenter = new ExclusiveBottomSheetPresenter();
@Nullable private PageTitle pageTitle;
@Nullable private WikiSite wiki;
@NonNull private GalleryCollectionClient client = new GalleryCollectionClient();
private ViewGroup toolbarContainer;
private ViewGroup infoContainer;
private ProgressBar progressBar;
private TextView descriptionText;
private ImageView licenseIcon;
private TextView creditText;
private WikiErrorView errorView;
private boolean controlsShowing = true;
@Nullable private ViewPager.OnPageChangeListener pageChangeListener;
@Nullable private GalleryFunnel funnel;
@Nullable protected GalleryFunnel getFunnel() {
return funnel;
}
/**
* If we have an intent that tells us a specific image to jump to within the gallery,
* then this will be non-null.
*/
private String initialFilename;
/**
* If we come back from savedInstanceState, then this will be the previous pager position.
*/
private int initialImageIndex = -1;
private ViewPager galleryPager;
private GalleryItemAdapter galleryAdapter;
private MediaDownloadReceiver downloadReceiver;
/**
* Cache that stores GalleryItem information for each corresponding media item in
* our gallery collection.
*/
@Nullable private Map<PageTitle, GalleryItem> galleryCache;
@Nullable
public Map<PageTitle, GalleryItem> getGalleryCache() {
return galleryCache;
}
private View.OnClickListener licenseShortClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (v.getContentDescription() == null) {
return;
}
FeedbackUtil.showMessageAsPlainText((Activity) v.getContext(), v.getContentDescription());
}
};
private View.OnLongClickListener licenseLongClickListener = new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
String licenseUrl = (String) v.getTag();
if (!TextUtils.isEmpty(licenseUrl)) {
handleExternalLink(GalleryActivity.this, Uri.parse(resolveProtocolRelativeUrl(licenseUrl)));
}
return true;
}
};
@NonNull
public static Intent newIntent(@NonNull Context context, int age, @NonNull String filename,
@NonNull FeaturedImage image, @NonNull WikiSite wiki, int source) {
return newIntent(context, null, filename, wiki, source)
.putExtra(EXTRA_FEATURED_IMAGE, GsonMarshaller.marshal(image))
.putExtra(EXTRA_FEATURED_IMAGE_AGE, age);
}
@NonNull
public static Intent newIntent(@NonNull Context context, @Nullable PageTitle pageTitle,
@NonNull String filename, @NonNull WikiSite wiki, int source) {
Intent intent = new Intent()
.setClass(context, GalleryActivity.class)
.putExtra(EXTRA_FILENAME, filename)
.putExtra(EXTRA_WIKI, wiki)
.putExtra(EXTRA_SOURCE, source);
if (pageTitle != null) {
intent.putExtra(EXTRA_PAGETITLE, pageTitle);
}
return intent;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// force the theme to dark...
setTheme(Theme.DARK.getResourceId());
downloadReceiver = new MediaDownloadReceiver(this);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_gallery);
initToolbar();
toolbarContainer = (ViewGroup) findViewById(R.id.gallery_toolbar_container);
infoContainer = (ViewGroup) findViewById(R.id.gallery_info_container);
setBackgroundGradient(infoContainer, Gravity.BOTTOM);
progressBar = (ProgressBar) findViewById(R.id.gallery_progressbar);
descriptionText = (TextView) findViewById(R.id.gallery_description_text);
descriptionText.setShadowLayer(2, 1, 1, color(R.color.lead_text_shadow));
descriptionText.setMovementMethod(linkMovementMethod);
licenseIcon = (ImageView) findViewById(R.id.gallery_license_icon);
licenseIcon.setOnClickListener(licenseShortClickListener);
licenseIcon.setOnLongClickListener(licenseLongClickListener);
creditText = (TextView) findViewById(R.id.gallery_credit_text);
creditText.setShadowLayer(2, 1, 1, color(R.color.lead_text_shadow));
errorView = (WikiErrorView) findViewById(R.id.view_gallery_error);
errorView.setBackClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onBackPressed();
}
});
errorView.setRetryClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
errorView.setVisibility(View.GONE);
loadGalleryContent();
}
});
if (getIntent().hasExtra(EXTRA_PAGETITLE)) {
pageTitle = getIntent().getParcelableExtra(EXTRA_PAGETITLE);
}
initialFilename = getIntent().getStringExtra(EXTRA_FILENAME);
wiki = getIntent().getParcelableExtra(EXTRA_WIKI);
galleryCache = new HashMap<>();
galleryAdapter = new GalleryItemAdapter(this);
galleryPager = (ViewPager) findViewById(R.id.gallery_item_pager);
galleryPager.setAdapter(galleryAdapter);
pageChangeListener = new GalleryPageChangeListener();
galleryPager.addOnPageChangeListener(pageChangeListener);
funnel = new GalleryFunnel(app, wiki, getIntent().getIntExtra(EXTRA_SOURCE, 0));
if (savedInstanceState == null) {
if (initialFilename != null) {
funnel.logGalleryOpen(pageTitle, initialFilename);
}
} else {
controlsShowing = savedInstanceState.getBoolean("controlsShowing");
initialImageIndex = savedInstanceState.getInt("pagerIndex");
// if we have a savedInstanceState, then the initial index overrides
// the initial Title from our intent.
initialFilename = null;
FragmentManager fm = getSupportFragmentManager();
if (getSupportFragmentManager().getBackStackEntryCount() > 0) {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
for (int i = 0; i < fm.getBackStackEntryCount(); i++) {
Fragment fragment = fm.findFragmentById(fm.getBackStackEntryAt(i).getId());
if (fragment instanceof GalleryItemFragment) {
ft.remove(fragment);
}
}
ft.commitAllowingStateLoss();
}
}
toolbarContainer.post(new Runnable() {
@Override
public void run() {
setControlsShowing(controlsShowing);
}
});
loadGalleryContent();
}
@Override public void onDestroy() {
galleryPager.removeOnPageChangeListener(pageChangeListener);
pageChangeListener = null;
super.onDestroy();
}
private void loadGalleryItemFor(@NonNull FeaturedImage image, int age) {
List<GalleryItem> list = new ArrayList<>();
list.add(new FeaturedImageGalleryItem(image, age));
applyGalleryCollection(new GalleryCollection(list));
}
@Override
public void onResume() {
super.onResume();
registerReceiver(downloadReceiver, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
@Override
public void onPause() {
super.onPause();
unregisterReceiver(downloadReceiver);
}
private class GalleryPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
private int currentPosition = -1;
@Override
public void onPageSelected(int position) {
// the pager has settled on a new position
layOutGalleryDescription();
galleryAdapter.notifyFragments(position);
if (currentPosition != -1 && getCurrentItem() != null) {
if (position < currentPosition) {
funnel.logGallerySwipeLeft(pageTitle, getCurrentItem().getName());
} else if (position > currentPosition) {
funnel.logGallerySwipeRight(pageTitle, getCurrentItem().getName());
}
}
currentPosition = position;
}
@Override
public void onPageScrollStateChanged(int state) {
if (state == ViewPager.SCROLL_STATE_IDLE) {
galleryAdapter.purgeFragments(false);
}
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("controlsShowing", controlsShowing);
outState.putInt("pagerIndex", galleryPager.getCurrentItem());
}
private void updateProgressBar(boolean visible, boolean indeterminate, int value) {
progressBar.setIndeterminate(indeterminate);
if (!indeterminate) {
progressBar.setProgress(value);
}
progressBar.setVisibility(visible ? View.VISIBLE : View.GONE);
}
@Override
public void onBackPressed() {
// log the "gallery close" event only upon explicit closing of the activity
// (back button, or home-as-up button in the toolbar)
if (getCurrentItem() != null) {
funnel.logGalleryClose(pageTitle, getCurrentItem().getName());
}
super.onBackPressed();
}
public MediaDownloadReceiver getDownloadReceiver() {
return downloadReceiver;
}
/**
* Show or hide all the UI controls in this activity (slide them out or in).
* @param showing Whether to show or hide the controls.
*/
private void setControlsShowing(boolean showing) {
controlsShowing = showing;
if (controlsShowing) {
ViewAnimations.ensureTranslationY(toolbarContainer, 0);
ViewAnimations.ensureTranslationY(infoContainer, 0);
} else {
ViewAnimations.ensureTranslationY(toolbarContainer, -toolbarContainer.getHeight());
ViewAnimations.ensureTranslationY(infoContainer, infoContainer.getHeight());
}
}
/**
* Toggle showing or hiding of all the UI controls.
*/
public void toggleControls() {
setControlsShowing(!controlsShowing);
}
public void showLinkPreview(@NonNull PageTitle title) {
bottomSheetPresenter.show(getSupportFragmentManager(),
LinkPreviewDialog.newInstance(title, HistoryEntry.SOURCE_GALLERY, null));
}
/**
* LinkMovementMethod for handling clicking of links in the description or metadata
* text fields. For internal links, this activity will close, and pass the page title as
* the result. For external links, they will be bounced out to the Browser.
*/
private LinkMovementMethodExt linkMovementMethod =
new LinkMovementMethodExt(new LinkMovementMethodExt.UrlHandler() {
@Override
public void onUrlClick(@NonNull String url, @Nullable String notUsed) {
L.v("Link clicked was " + url);
url = resolveProtocolRelativeUrl(url);
WikiSite appWikiSite = app.getWikiSite();
if (url.startsWith("/wiki/")) {
PageTitle title = appWikiSite.titleForInternalLink(url);
showLinkPreview(title);
} else {
Uri uri = Uri.parse(url);
String authority = uri.getAuthority();
if (authority != null && WikiSite.supportedAuthority(authority)
&& uri.getPath().startsWith("/wiki/")) {
PageTitle title = appWikiSite.titleForUri(uri);
showLinkPreview(title);
} else {
// if it's a /w/ URI, turn it into a full URI and go external
if (url.startsWith("/w/")) {
url = String.format("%1$s://%2$s", appWikiSite.scheme(), appWikiSite.authority()) + url;
}
handleExternalLink(GalleryActivity.this, Uri.parse(url));
}
}
}
});
/**
* Close this activity, with the specified PageTitle as the activity result, to be picked up
* by the activity that originally launched us.
* @param resultTitle PageTitle to pass as the activity result.
*/
public void finishWithPageResult(@NonNull PageTitle resultTitle) {
finishWithPageResult(resultTitle, new HistoryEntry(resultTitle, HistoryEntry.SOURCE_GALLERY));
}
public void finishWithPageResult(@NonNull PageTitle resultTitle, @NonNull HistoryEntry historyEntry) {
Intent intent = PageActivity.newIntent(GalleryActivity.this, historyEntry, resultTitle);
setResult(ACTIVITY_RESULT_PAGE_SELECTED, intent);
finish();
}
@Override public void onLinkPreviewLoadPage(@NonNull PageTitle title, @NonNull HistoryEntry entry, boolean inNewTab) {
finishWithPageResult(title, entry);
}
@Override public void onLinkPreviewCopyLink(@NonNull PageTitle title) {
ClipboardUtil.setPlainText(this, null, title.getCanonicalUri());
FeedbackUtil.showMessage(this, R.string.address_copied);
}
@Override public void onLinkPreviewAddToList(@NonNull PageTitle title) {
bottomSheetPresenter.showAddToListDialog(getSupportFragmentManager(), title, AddToReadingListDialog.InvokeSource.LINK_PREVIEW_MENU);
}
@Override public void onLinkPreviewShareLink(@NonNull PageTitle title) {
ShareUtil.shareText(this, title);
}
void showError(@Nullable Throwable caught, boolean backOnError) {
// Force going back on button press if coming from a single-item featured-image gallery,
// because re-setting the collection and calling notifyDataSetChanged() fails to compel the
// GalleryItemFragment to attempt to reload its image.
// TODO: Find a way to remove this workaround
if (backOnError) {
errorView.setRetryClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onBackPressed();
}
});
}
errorView.setError(caught);
errorView.setVisibility(View.VISIBLE);
}
/**
* Kicks off the activity after the views are initialized in onCreate.
*/
private void loadGalleryContent() {
updateProgressBar(false, true, 0);
if (getIntent().hasExtra(EXTRA_FEATURED_IMAGE)) {
FeaturedImage featuredImage = GsonUnmarshaller.unmarshal(FeaturedImage.class,
getIntent().getStringExtra(EXTRA_FEATURED_IMAGE));
int age = getIntent().getIntExtra(EXTRA_FEATURED_IMAGE_AGE, 0);
loadGalleryItemFor(featuredImage, age);
} else {
fetchGalleryCollection();
}
}
/**
* Retrieve the complete list of media items for the current page.
* When retrieved, the list will be passed to the ViewPager, and will become a
* scrollable gallery of media.
*/
private void fetchGalleryCollection() {
if (pageTitle == null) {
return;
}
updateProgressBar(true, true, 0);
CallbackTask.execute(new CallbackTask.Task<Map<String, ImageInfo>>() {
@Override public Map<String, ImageInfo> execute() throws Throwable {
return client.request(pageTitle.getWikiSite(), pageTitle, false);
}
}, new CallbackTask.Callback<Map<String, ImageInfo>>() {
@Override public void success(Map<String, ImageInfo> result) {
updateProgressBar(false, true, 0);
applyGalleryCollection(buildCollection(result));
}
@Override public void failure(Throwable caught) {
updateProgressBar(false, true, 0);
showError(caught, false);
}
});
}
@NonNull private GalleryCollection buildCollection(Map<String, ImageInfo> result) {
List<GalleryItem> list = new ArrayList<>();
for (Map.Entry<String, ImageInfo> entry : result.entrySet()) {
if (GalleryCollection.shouldIncludeImage(entry.getValue())) {
list.add(new GalleryItem(entry.getKey(), entry.getValue()));
}
}
return new GalleryCollection(list);
}
/**
* Apply a complete collection of media to our scrollable gallery.
* @param collection GalleryCollection to apply to the ViewPager.
*/
private void applyGalleryCollection(@NonNull GalleryCollection collection) {
// remove the page transformer while we operate on the pager...
galleryPager.setPageTransformer(false, null);
// first, verify that the collection contains the item that the user
// initially requested, if we have one...
int initialImagePos = -1;
if (initialFilename != null) {
for (GalleryItem item : collection.getItemList()) {
if (item.getName().equals(initialFilename)) {
initialImagePos = collection.getItemList().indexOf(item);
break;
}
}
if (initialImagePos == -1) {
// the requested image is not present in the gallery collection, so
// add it manually.
// (this can happen if the user clicked on an SVG file, since we hide SVGs
// by default in the gallery)
initialImagePos = 0;
collection.getItemList().add(initialImagePos,
new GalleryItem(initialFilename));
}
}
// pass the collection to the adapter!
galleryAdapter.setCollection(collection);
if (initialImagePos != -1) {
// if we have a target image to jump to, then do it!
galleryPager.setCurrentItem(initialImagePos, false);
} else if (initialImageIndex >= 0
&& initialImageIndex < galleryAdapter.getCount()) {
// if we have a target image index to jump to, then do it!
galleryPager.setCurrentItem(initialImageIndex, false);
}
galleryPager.setPageTransformer(false, new GalleryPagerTransformer());
}
private GalleryItem getCurrentItem() {
if (galleryAdapter.getItem(galleryPager.getCurrentItem()) == null) {
return null;
}
return ((GalleryItemFragment) galleryAdapter.getItem(galleryPager.getCurrentItem()))
.getGalleryItem();
}
/**
* Populate the description and license text fields with data from the current gallery item.
*/
public void layOutGalleryDescription() {
GalleryItem item = getCurrentItem();
if (item == null) {
infoContainer.setVisibility(View.GONE);
return;
}
galleryAdapter.notifyFragments(galleryPager.getCurrentItem());
CharSequence descriptionStr = "";
if (item.getMetadata().containsKey("ImageDescription")) {
descriptionStr = StringUtil.fromHtml(item.getMetadata().get("ImageDescription"));
} else if (item.getMetadata().containsKey("ObjectName")) {
descriptionStr = StringUtil.fromHtml(item.getMetadata().get("ObjectName"));
}
if (descriptionStr.length() > 0) {
descriptionText.setText(strip(descriptionStr));
descriptionText.setVisibility(View.VISIBLE);
} else {
descriptionText.setVisibility(View.GONE);
}
// determine which icon to display...
licenseIcon.setImageResource(getLicenseIcon(item));
// Set the icon's content description to the UsageTerms property.
// (if UsageTerms is not present, then default to Fair Use)
String usageTerms = item.getMetadata().get("UsageTerms");
if (TextUtils.isEmpty(usageTerms)) {
usageTerms = getString(R.string.gallery_fair_use_license);
}
licenseIcon.setContentDescription(usageTerms);
// Give the license URL to the icon, to be received by the click handler (may be null).
licenseIcon.setTag(item.getLicenseUrl());
String creditStr = "";
if (item.getMetadata().containsKey("Artist")) {
// todo: is it intentional to convert to a String?
creditStr = StringUtil.fromHtml(item.getMetadata().get("Artist")).toString().trim();
}
// if we couldn't find a attribution string, then default to unknown
if (TextUtils.isEmpty(creditStr)) {
creditStr = getString(R.string.gallery_uploader_unknown);
}
creditText.setText(creditStr);
infoContainer.setVisibility(View.VISIBLE);
}
/**
* Return an icon (drawable resource id) that corresponds to the type of license
* under which the specified Gallery item is provided.
* @param item Gallery item for which to give a license icon.
* @return Resource ID of the icon to display, or 0 if no license is available.
*/
private static int getLicenseIcon(GalleryItem item) {
return item.getLicense().getLicenseIcon();
}
private void setBackgroundGradient(View view, int gravity) {
ViewUtil.setBackgroundDrawable(view, GradientUtil.getCubicGradient(
color(R.color.lead_gradient_start), gravity));
}
private void initToolbar() {
final Toolbar toolbar = (Toolbar) findViewById(R.id.gallery_toolbar);
setBackgroundGradient(toolbar, Gravity.TOP);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle("");
}
/**
* Adapter that will provide the contents for the ViewPager.
* Each media item will be represented by a GalleryItemFragment, which will be instantiated
* lazily, and then cached for future use.
*/
private class GalleryItemAdapter extends FragmentPagerAdapter {
private GalleryCollection galleryCollection;
private SparseArray<GalleryItemFragment> fragmentArray;
GalleryItemAdapter(ThemedActionBarActivity activity) {
super(activity.getSupportFragmentManager());
fragmentArray = new SparseArray<>();
}
public void setCollection(@NonNull GalleryCollection collection) {
galleryCollection = collection;
notifyDataSetChanged();
}
public void notifyFragments(int currentPosition) {
for (int i = 0; i < getCount(); i++) {
if (fragmentArray.get(i) != null) {
fragmentArray.get(i).onUpdatePosition(i, currentPosition);
}
}
}
/**
* Remove any active fragments to the left+1 and right+1 of the current
* fragment, to reduce memory usage.
*/
public void purgeFragments(boolean removeAll) {
int position = galleryPager.getCurrentItem();
FragmentTransaction trans = getSupportFragmentManager().beginTransaction();
for (int i = 0; i < getCount(); i++) {
if (!removeAll && Math.abs(position - i) < 2) {
continue;
}
if (fragmentArray.get(i) != null) {
trans.remove(fragmentArray.get(i));
fragmentArray.remove(i);
fragmentArray.put(i, null);
}
}
trans.commitAllowingStateLoss();
}
@Override
public int getCount() {
return galleryCollection == null ? 0 : galleryCollection.getItemList().size();
}
@Override
public Fragment getItem(int position) {
if (galleryCollection == null || galleryCollection.getItemList().size() <= position) {
return null;
}
// instantiate a new fragment if it doesn't exist
if (fragmentArray.get(position) == null) {
fragmentArray.put(position, GalleryItemFragment
.newInstance(pageTitle, wiki, galleryCollection.getItemList().get(position)));
}
return fragmentArray.get(position);
}
}
@ColorInt private int color(@ColorRes int id) {
return ContextCompat.getColor(this, id);
}
}