/*
* Overchan Android (Meta Imageboard Client)
* Copyright (C) 2014-2016 miku-nyan <https://github.com/miku-nyan>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package nya.miku.wishmaster.ui.gallery;
import java.io.File;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.commons.lang3.tuple.Triple;
import nya.miku.wishmaster.R;
import nya.miku.wishmaster.api.interfaces.CancellableTask;
import nya.miku.wishmaster.api.interfaces.ProgressListener;
import nya.miku.wishmaster.api.models.AttachmentModel;
import nya.miku.wishmaster.api.models.BoardModel;
import nya.miku.wishmaster.common.Async;
import nya.miku.wishmaster.common.Logger;
import nya.miku.wishmaster.lib.gallery.FixedSubsamplingScaleImageView;
import nya.miku.wishmaster.lib.gallery.JSWebView;
import nya.miku.wishmaster.lib.gallery.Jpeg;
import nya.miku.wishmaster.lib.gallery.TouchGifView;
import nya.miku.wishmaster.lib.gallery.WebViewFixed;
import nya.miku.wishmaster.lib.gallery.verticalviewpager.VerticalViewPagerFixed;
import nya.miku.wishmaster.lib.gifdrawable.GifDrawable;
import nya.miku.wishmaster.ui.AppearanceUtils;
import nya.miku.wishmaster.ui.Attachments;
import nya.miku.wishmaster.ui.CompatibilityImpl;
import nya.miku.wishmaster.ui.ReverseImageSearch;
import nya.miku.wishmaster.ui.downloading.DownloadingService;
import nya.miku.wishmaster.ui.presentation.BoardFragment;
import nya.miku.wishmaster.ui.settings.ApplicationSettings;
import nya.miku.wishmaster.ui.tabs.UrlHandler;
import nya.miku.wishmaster.ui.theme.ThemeUtils;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Point;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
import android.util.SparseArray;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.VideoView;
public class GalleryActivity extends Activity implements View.OnClickListener {
private static final String TAG = "GalleryActivity";
public static final String EXTRA_SETTINGS = "settings";
public static final String EXTRA_ATTACHMENT = "attachment";
public static final String EXTRA_SAVED_ATTACHMENTHASH = "attachmenthash";
public static final String EXTRA_BOARDMODEL = "boardmodel";
public static final String EXTRA_PAGEHASH = "pagehash";
public static final String EXTRA_LOCALFILENAME = "localfilename";
@SuppressLint("InlinedApi")
private static final int BINDING_FLAGS = Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT;
private static final int REQUEST_HANDLE_INTERACTIVE_EXCEPTION = 1;
private LayoutInflater inflater;
private ExecutorService tnDownloadingExecutor;
private BoardModel boardModel;
private String chan;
private ProgressBar progressBar;
private ViewPager viewPager;
private TextView navigationInfo;
private SparseArray<View> instantiatedViews;
private BroadcastReceiver broadcastReceiver;
private ServiceConnection serviceConnection;
private GalleryRemote remote;
private GallerySettings settings;
private List<Triple<AttachmentModel, String, String>> attachments = null;
private int currentPosition = 0;
private int previousPosition = -1;
private boolean firstScroll = true;
private Menu menu;
private boolean currentLoaded;
private static class ProgressHandler extends Handler {
private final WeakReference<GalleryActivity> reference;
public ProgressHandler(GalleryActivity activity) {
reference = new WeakReference<GalleryActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
GalleryActivity activity = reference.get();
if (activity == null) return;
int progress = msg.arg1;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (progress != Window.PROGRESS_END) {
if (activity.progressBar.getVisibility() == View.GONE) activity.progressBar.setVisibility(View.VISIBLE);
activity.progressBar.setProgress(progress);
} else {
if (activity.progressBar.getVisibility() == View.VISIBLE) activity.progressBar.setVisibility(View.GONE);
}
} else {
activity.setProgress(progress);
}
}
}
private ProgressListener progressListener = new ProgressListener() {
private long maxValue = Window.PROGRESS_END;
private Handler progressHandler = new ProgressHandler(GalleryActivity.this);
@Override
public void setProgress(final long value) {
progressHandler.obtainMessage(0, (int)(Window.PROGRESS_END * value / maxValue), 0).sendToTarget();
}
@Override
public void setMaxValue(long value) {
if (value > 0) maxValue = value;
}
@Override
public void setIndeterminate() {
}
};
private void hideProgress() {
progressListener.setMaxValue(1);
progressListener.setProgress(1);
}
private abstract class AbstractGetterCallback extends GalleryGetterCallback.Stub {
private final CancellableTask task;
public AbstractGetterCallback(CancellableTask task) {
this.task = task;
}
@Override
public boolean isTaskCancelled() throws RemoteException {
return task.isCancelled();
}
@Override
public void setProgress(long value) throws RemoteException {
progressListener.setProgress(value);
}
@Override
public void setProgressIndeterminate() throws RemoteException {
progressListener.setIndeterminate();
}
@Override
public void setProgressMaxValue(long value) throws RemoteException {
progressListener.setMaxValue(value);
}
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) requestWindowFeature(Window.FEATURE_PROGRESS);
settings = getIntent().getParcelableExtra(EXTRA_SETTINGS);
if (settings == null) settings = GallerySettings.fromSettings(
new ApplicationSettings(PreferenceManager.getDefaultSharedPreferences(getApplication()), getResources()));
settings.getTheme().setTo(this, R.style.Transparent);
super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) CompatibilityImpl.setActionBarNoIcon(this);
inflater = getLayoutInflater();
instantiatedViews = new SparseArray<View>();
tnDownloadingExecutor = Executors.newFixedThreadPool(4, Async.LOW_PRIORITY_FACTORY);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH && settings.fullscreenGallery()) {
setContentView(R.layout.gallery_layout_fullscreen);
GalleryFullscreen.initFullscreen(this);
} else {
setContentView(R.layout.gallery_layout);
}
progressBar = (ProgressBar) findViewById(android.R.id.progress);
progressBar.setMax(Window.PROGRESS_END);
viewPager = (ViewPager) findViewById(R.id.gallery_viewpager);
navigationInfo = (TextView) findViewById(R.id.gallery_navigation_info);
for (int id : new int[] { R.id.gallery_navigation_previous, R.id.gallery_navigation_next }) findViewById(id).setOnClickListener(this);
bindService(new Intent(this, GalleryBackend.class), new ServiceConnection() {
{ serviceConnection = this; }
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
GalleryBinder galleryBinder = GalleryBinder.Stub.asInterface(service);
try {
GalleryInitData initData = new GalleryInitData(getIntent(), savedInstanceState);
boardModel = initData.boardModel;
chan = boardModel.chan;
remote = new GalleryRemote(galleryBinder, galleryBinder.initContext(initData));
GalleryInitResult initResult = remote.getInitResult();
if (initResult != null) {
attachments = initResult.attachments;
currentPosition = initResult.initPosition;
if (initResult.shouldWaitForPageLoaded) waitForPageLoaded(savedInstanceState);
} else {
attachments = Collections.singletonList(Triple.of(initData.attachment, initData.attachmentHash, (String)null));
currentPosition = 0;
}
viewPager.setAdapter(new GalleryAdapter());
viewPager.setCurrentItem(currentPosition);
viewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
currentPosition = position;
updateItem();
}
});
} catch (Exception e) {
Logger.e(TAG, e);
finish();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Logger.e(TAG, "backend service disconnected");
remote = null;
System.exit(0);
}
}, BINDING_FLAGS);
GalleryExceptionHandler.init();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(EXTRA_SAVED_ATTACHMENTHASH, attachments.get(currentPosition).getMiddle());
}
private void waitForPageLoaded(Bundle savedInstanceState) {
final String savedHash = savedInstanceState != null ? savedInstanceState.getString(EXTRA_SAVED_ATTACHMENTHASH) : null;
if (savedHash != null) registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction() != null && intent.getAction().equals(BoardFragment.BROADCAST_PAGE_LOADED)) {
unregisterReceiver(this);
broadcastReceiver = null;
Intent activityIntent = getIntent();
String pagehash = activityIntent.getStringExtra(EXTRA_PAGEHASH);
if (pagehash != null && remote.isPageLoaded(pagehash)) {
startActivity(activityIntent.putExtra(EXTRA_SAVED_ATTACHMENTHASH, savedHash));
finish();
}
}
}
}, new IntentFilter(BoardFragment.BROADCAST_PAGE_LOADED));
}
@Override
protected void onStop() {
super.onStop();
BroadcastReceiver receiver = broadcastReceiver;
if (receiver != null) unregisterReceiver(receiver);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (instantiatedViews != null) {
for (int i=0; i<instantiatedViews.size(); ++i) {
View v = instantiatedViews.valueAt(i);
if (v != null) {
Object tag = v.getTag();
if (tag != null && tag instanceof GalleryItemViewTag) {
recycleTag((GalleryItemViewTag) tag, true);
}
}
}
}
tnDownloadingExecutor.shutdown();
if (serviceConnection != null) unbindService(serviceConnection);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.gallery_navigation_previous:
if (currentPosition > 0) {
viewPager.setCurrentItem(--currentPosition);
updateItem();
}
break;
case R.id.gallery_navigation_next:
if (currentPosition < attachments.size() - 1) {
viewPager.setCurrentItem(++currentPosition);
updateItem();
}
break;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
this.menu = menu;
MenuItem itemUpdate = menu.add(Menu.NONE, R.id.menu_update, 1, R.string.menu_update);
MenuItem itemSave = menu.add(Menu.NONE, R.id.menu_save_attachment, 2, R.string.menu_save_attachment);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
itemUpdate.setIcon(ThemeUtils.getActionbarIcon(getTheme(), getResources(), R.attr.actionRefresh));
itemSave.setIcon(ThemeUtils.getActionbarIcon(getTheme(), getResources(), R.attr.actionSave));
CompatibilityImpl.setShowAsActionIfRoom(itemUpdate);
CompatibilityImpl.setShowAsActionIfRoom(itemSave);
} else {
itemUpdate.setIcon(R.drawable.ic_menu_refresh);
itemSave.setIcon(android.R.drawable.ic_menu_save);
}
menu.add(Menu.NONE, R.id.menu_open_external, 3, R.string.menu_open).setIcon(R.drawable.ic_menu_set_as);
menu.add(Menu.NONE, R.id.menu_share, 4, R.string.menu_share).setIcon(android.R.drawable.ic_menu_share);
menu.add(Menu.NONE, R.id.menu_share_link, 5, R.string.menu_share_link).setIcon(android.R.drawable.ic_menu_share);
menu.add(Menu.NONE, R.id.menu_reverse_search, 6, R.string.menu_reverse_search).setIcon(android.R.drawable.ic_menu_search);
menu.add(Menu.NONE, R.id.menu_open_browser, 7, R.string.menu_open_browser).setIcon(R.drawable.ic_menu_browser);
updateMenu();
return true;
}
private void updateMenu() {
if (this.menu == null) return;
View current = instantiatedViews.get(currentPosition);
if (current == null) {
Logger.e(TAG, "VIEW == NULL");
return;
}
GalleryItemViewTag tag = (GalleryItemViewTag) current.getTag();
boolean externalVideo = tag.attachmentModel.type == AttachmentModel.TYPE_VIDEO && settings.doNotDownloadVideos();
menu.findItem(R.id.menu_update).setVisible(!currentLoaded);
menu.findItem(R.id.menu_save_attachment).setVisible(externalVideo ||
(currentLoaded && tag.attachmentModel.type != AttachmentModel.TYPE_OTHER_NOTFILE));
menu.findItem(R.id.menu_open_external).setVisible(currentLoaded && (tag.attachmentModel.type == AttachmentModel.TYPE_OTHER_FILE ||
tag.attachmentModel.type == AttachmentModel.TYPE_AUDIO || tag.attachmentModel.type == AttachmentModel.TYPE_VIDEO));
menu.findItem(R.id.menu_open_external).setTitle(tag.attachmentModel.type != AttachmentModel.TYPE_OTHER_FILE ?
R.string.menu_open_player : R.string.menu_open);
menu.findItem(R.id.menu_share).setVisible(currentLoaded && tag.attachmentModel.type != AttachmentModel.TYPE_OTHER_NOTFILE);
menu.findItem(R.id.menu_reverse_search).setVisible(
tag.attachmentModel.type == AttachmentModel.TYPE_IMAGE_STATIC || tag.attachmentModel.type == AttachmentModel.TYPE_IMAGE_GIF);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_update:
updateItem();
return true;
case R.id.menu_save_attachment:
downloadAttachment();
return true;
case R.id.menu_open_external:
openExternal();
return true;
case R.id.menu_share:
share();
return true;
case R.id.menu_share_link:
shareLink();
return true;
case R.id.menu_reverse_search:
reverseSearch();
return true;
case R.id.menu_open_browser:
openBrowser();
return true;
}
return false;
}
private GalleryItemViewTag getCurrentTag() {
View current = instantiatedViews.get(currentPosition);
if (current == null) {
Logger.e(TAG, "VIEW == NULL (position=" + currentPosition + ")");
return null;
}
return (GalleryItemViewTag) current.getTag();
}
private void downloadAttachment() {
GalleryItemViewTag tag = getCurrentTag();
if (tag == null) return;
DownloadingService.DownloadingQueueItem queueItem = new DownloadingService.DownloadingQueueItem(tag.attachmentModel, boardModel);
String fileName = Attachments.getAttachmentLocalFileName(tag.attachmentModel, boardModel);
String itemName = Attachments.getAttachmentLocalShortName(tag.attachmentModel, boardModel);
if (DownloadingService.isInQueue(queueItem)) {
Toast.makeText(this, getString(R.string.notification_download_already_in_queue, itemName), Toast.LENGTH_LONG).show();
} else {
if (new File(new File(settings.getDownloadDirectory(), chan), fileName).exists()) {
Toast.makeText(this, getString(R.string.notification_download_already_exists, fileName), Toast.LENGTH_LONG).show();
} else {
Intent downloadIntent = new Intent(this, DownloadingService.class);
downloadIntent.putExtra(DownloadingService.EXTRA_DOWNLOADING_ITEM, queueItem);
startService(downloadIntent);
}
}
}
private void openExternal() {
GalleryItemViewTag tag = getCurrentTag();
if (tag == null) return;
String mime;
switch (tag.attachmentModel.type) {
case AttachmentModel.TYPE_VIDEO:
mime = "video/*";
break;
case AttachmentModel.TYPE_AUDIO:
mime = "audio/*";
break;
default:
mime = "*/*";
break;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(tag.file), mime);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
private void share() {
GalleryItemViewTag tag = getCurrentTag();
if (tag == null) return;
Intent shareIntent = new Intent(Intent.ACTION_SEND);
String extension = Attachments.getAttachmentExtention(tag.attachmentModel);
switch (tag.attachmentModel.type) {
case AttachmentModel.TYPE_IMAGE_GIF:
shareIntent.setType("image/gif");
break;
case AttachmentModel.TYPE_IMAGE_SVG:
shareIntent.setType("image/svg+xml");
break;
case AttachmentModel.TYPE_IMAGE_STATIC:
if (extension.equalsIgnoreCase(".png")) {
shareIntent.setType("image/png");
} else if (extension.equalsIgnoreCase(".jpg") || extension.equalsIgnoreCase(".jpg")) {
shareIntent.setType("image/jpeg");
} else {
shareIntent.setType("image/*");
}
break;
case AttachmentModel.TYPE_VIDEO:
if (extension.equalsIgnoreCase(".mp4")) {
shareIntent.setType("video/mp4");
} else if (extension.equalsIgnoreCase(".webm")) {
shareIntent.setType("video/webm");
} else if (extension.equalsIgnoreCase(".avi")) {
shareIntent.setType("video/avi");
} else if (extension.equalsIgnoreCase(".mov")) {
shareIntent.setType("video/quicktime");
} else if (extension.equalsIgnoreCase(".mkv")) {
shareIntent.setType("video/x-matroska");
} else if (extension.equalsIgnoreCase(".flv")) {
shareIntent.setType("video/x-flv");
} else if (extension.equalsIgnoreCase(".wmv")) {
shareIntent.setType("video/x-ms-wmv");
} else {
shareIntent.setType("video/*");
}
break;
case AttachmentModel.TYPE_AUDIO:
if (extension.equalsIgnoreCase(".mp3")) {
shareIntent.setType("audio/mpeg");
} else if (extension.equalsIgnoreCase(".mp4")) {
shareIntent.setType("audio/mp4");
} else if (extension.equalsIgnoreCase(".ogg")) {
shareIntent.setType("audio/ogg");
} else if (extension.equalsIgnoreCase(".webm")) {
shareIntent.setType("audio/webm");
} else if (extension.equalsIgnoreCase(".flac")) {
shareIntent.setType("audio/flac");
} else if (extension.equalsIgnoreCase(".wav")) {
shareIntent.setType("audio/vnd.wave");
} else {
shareIntent.setType("audio/*");
}
break;
case AttachmentModel.TYPE_OTHER_FILE:
shareIntent.setType("application/octet-stream");
break;
}
Logger.d(TAG, shareIntent.getType());
shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(tag.file));
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_via)));
}
private void shareLink() {
GalleryItemViewTag tag = getCurrentTag();
if (tag == null) return;
String absoluteUrl = remote.getAbsoluteUrl(tag.attachmentModel.path);
if (absoluteUrl == null) return;
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_SUBJECT, absoluteUrl);
shareIntent.putExtra(Intent.EXTRA_TEXT, absoluteUrl);
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_via)));
}
private void reverseSearch() {
GalleryItemViewTag tag = getCurrentTag();
if (tag == null) return;
String absoluteUrl = remote.getAbsoluteUrl(tag.attachmentModel.path);
if (absoluteUrl == null) return;
ReverseImageSearch.openDialog(this, absoluteUrl);
}
private void openBrowser() {
GalleryItemViewTag tag = getCurrentTag();
if (tag == null) return;
String absoluteUrl = remote.getAbsoluteUrl(tag.attachmentModel.path);
if (absoluteUrl == null) return;
UrlHandler.launchExternalBrowser(this, absoluteUrl);
}
private class GalleryAdapter extends PagerAdapter {
private boolean firstTime = true;
private final Runnable finishCallback = new Runnable() {
@Override
public void run() {
GalleryActivity.this.finish();
}
};
@Override
public int getCount() {
return attachments.size();
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
View v = inflater.inflate(R.layout.gallery_item, container, false);
GalleryItemViewTag tag = new GalleryItemViewTag();
tag.attachmentModel = attachments.get(position).getLeft();
tag.attachmentHash = attachments.get(position).getMiddle();
tag.thumbnailView = (ImageView) v.findViewById(R.id.gallery_thumbnail_preview);
int tnWidth = Math.min(container.getMeasuredWidth(), tag.attachmentModel.width * 2);
if (tnWidth > 0) tag.thumbnailView.getLayoutParams().width = tnWidth;
tag.layout = (FrameLayout) v.findViewById(R.id.gallery_item_layout);
tag.errorView = v.findViewById(R.id.gallery_error);
tag.errorText = (TextView) tag.errorView.findViewById(R.id.frame_error_text);
tag.errorText.setTextColor(Color.WHITE);
tag.loadingView = v.findViewById(R.id.gallery_loading);
v.setTag(tag);
instantiatedViews.put(position, v);
String hash = tag.attachmentHash;
Bitmap bmp = remote.getBitmapFromMemory(hash);
if (bmp != null) {
tag.thumbnailView.setImageBitmap(bmp);
} else {
tnDownloadingExecutor.execute(new AsyncThumbnailDownloader(position, hash, tag.attachmentModel.thumbnail));
}
if (settings.swipeToCloseGallery()) v = VerticalViewPagerFixed.wrap(v, finishCallback, settings.fullscreenGallery());
container.addView(v);
if (firstTime && position == currentPosition) {
updateItem();
firstTime = false;
}
return v;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
View v = (View) object;
Object tag = v.getTag();
if (tag != null && tag instanceof View) tag = ((View) tag).getTag();
if (tag != null && tag instanceof GalleryItemViewTag) recycleTag((GalleryItemViewTag) tag, true);
container.removeView(v);
instantiatedViews.delete(position);
}
private class AsyncThumbnailDownloader implements Runnable {
private final int position;
private final String hash;
private final String url;
public AsyncThumbnailDownloader(int position, String hash, String url) {
this.position = position;
this.hash = hash;
this.url = url;
}
@Override
public void run() {
Bitmap bmp = remote.getBitmap(hash, url);
if (bmp != null) {
View v = instantiatedViews.get(position);
if (v != null) {
final ImageView tnView = ((GalleryItemViewTag) v.getTag()).thumbnailView;
final Bitmap bmpSet = bmp;
runOnUiThread(new Runnable() {
@Override
public void run() {
if (tnView != null) {
tnView.setImageBitmap(bmpSet);
}
}
});
}
}
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_HANDLE_INTERACTIVE_EXCEPTION && resultCode == RESULT_OK) updateItem();
}
private void updateItem() {
AttachmentModel attachment = attachments.get(currentPosition).getLeft();
if (settings.scrollThreadFromGallery() && !firstScroll) remote.tryScrollParent(attachments.get(currentPosition).getRight());
firstScroll = false;
String navText = attachment.size == -1 ? (currentPosition + 1) + "/" + attachments.size() :
(currentPosition + 1) + "/" + attachments.size() + " (" + Attachments.getAttachmentSizeString(attachment, getResources()) + ")";
navigationInfo.setText(navText);
setTitle(Attachments.getAttachmentDisplayName(attachment));
if (previousPosition != -1) {
View previous = instantiatedViews.get(previousPosition);
if (previous != null) {
GalleryItemViewTag tag = (GalleryItemViewTag) previous.getTag();
tag.thumbnailView.setVisibility(View.VISIBLE);
tag.layout.setVisibility(View.GONE);
tag.errorView.setVisibility(View.GONE);
tag.loadingView.setVisibility(View.GONE);
recycleTag(tag, true);
}
}
previousPosition = currentPosition;
GalleryItemViewTag tag = getCurrentTag();
if (tag == null) return;
currentLoaded = false;
updateMenu();
tag.downloadingTask = new AttachmentGetter(tag);
tag.loadingView.setVisibility(View.VISIBLE);
hideProgress();
Async.runAsync((Runnable) tag.downloadingTask);
}
private class AttachmentGetter extends CancellableTask.BaseCancellableTask implements Runnable {
private final GalleryItemViewTag tag;
public AttachmentGetter(GalleryItemViewTag tag) {
this.tag = tag;
}
@Override
public void run() {
if (tag.attachmentModel.type == AttachmentModel.TYPE_OTHER_NOTFILE ||
(settings.doNotDownloadVideos() && tag.attachmentModel.type == AttachmentModel.TYPE_VIDEO)) {
setExternalLink(tag);
return;
} else if (tag.attachmentModel.path == null || tag.attachmentModel.path.length() == 0) {
showError(tag, getString(R.string.gallery_error_incorrect_attachment));
return;
}
final String[] exception = new String[1];
File file = remote.getAttachment(new GalleryAttachmentInfo(tag.attachmentModel, tag.attachmentHash), new AbstractGetterCallback(this) {
@Override
public void showLoading() {
runOnUiThread(new Runnable() {
@Override
public void run() {
tag.loadingView.setVisibility(View.VISIBLE);
}
});
}
@Override
public void onException(String message) {
exception[0] = message;
}
@Override
public void onInteractiveException(GalleryInteractiveExceptionHolder holder) {
if (holder.e == null) return;
exception[0] = getString(R.string.error_interactive_cancelled_format, holder.e.getServiceName());
startActivityForResult(new Intent(GalleryActivity.this, GalleryInteractiveExceptionHandler.class).
putExtra(GalleryInteractiveExceptionHandler.EXTRA_INTERACTIVE_EXCEPTION, holder.e), REQUEST_HANDLE_INTERACTIVE_EXCEPTION);
}
});
if (isCancelled()) return;
if (file == null) {
showError(tag, exception[0]);
return;
}
tag.file = file;
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isCancelled()) return;
hideProgress();
currentLoaded = true;
updateMenu();
}
});
switch (tag.attachmentModel.type) {
case AttachmentModel.TYPE_IMAGE_STATIC:
setStaticImage(tag, file);
break;
case AttachmentModel.TYPE_IMAGE_GIF:
setGif(tag, file);
break;
case AttachmentModel.TYPE_IMAGE_SVG:
setSvg(tag, file);
break;
case AttachmentModel.TYPE_VIDEO:
setVideo(tag, file);
break;
case AttachmentModel.TYPE_AUDIO:
setAudio(tag, file);
break;
case AttachmentModel.TYPE_OTHER_FILE:
setOtherFile(tag, file);
break;
}
}
}
private void showError(final GalleryItemViewTag tag, final String message) {
if (tag.downloadingTask.isCancelled()) return;
runOnUiThread(new Runnable() {
@Override
public void run() {
if (tag.downloadingTask.isCancelled()) return;
hideProgress();
tag.layout.setVisibility(View.GONE);
recycleTag(tag, true);
tag.thumbnailView.setVisibility(View.GONE);
tag.loadingView.setVisibility(View.GONE);
tag.errorView.setVisibility(View.VISIBLE);
tag.errorText.setText(fixErrorMessage(message));
}
private String fixErrorMessage(String message) {
if (message == null || message.length() == 0) {
return getString(R.string.error_unknown);
}
return message;
}
});
}
private void recycleTag(GalleryItemViewTag tag, boolean cancelTask) {
if (tag.layout != null) {
for (int i=0; i<tag.layout.getChildCount(); ++i) {
View v = tag.layout.getChildAt(i);
if (v instanceof FixedSubsamplingScaleImageView) {
((FixedSubsamplingScaleImageView) v).recycle();
} else if (v != null && v.getId() == R.id.gallery_video_container) {
try {
((VideoView) v.findViewById(R.id.gallery_video_view)).stopPlayback();
} catch (Exception e) {
Logger.e(TAG, "cannot release videoview", e);
}
} else if (v != null) {
Object gifTag = v.getTag();
if (gifTag != null && gifTag instanceof GifDrawable) {
((GifDrawable) gifTag).recycle();
}
}
}
tag.layout.removeAllViews();
}
if (cancelTask && tag.downloadingTask != null) tag.downloadingTask.cancel();
if (tag.timer != null) tag.timer.cancel();
if (tag.audioPlayer != null) {
try {
tag.audioPlayer.release();
} catch (Exception e) {
Logger.e(TAG, "cannot release audio mediaplayer", e);
} finally {
tag.audioPlayer = null;
}
}
System.gc();
}
private void setStaticImage(final GalleryItemViewTag tag, final File file) {
if (!settings.useScaleImageView() || Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD_MR1 || Jpeg.isNonStandardGrayscaleImage(file)) {
setWebView(tag, file);
return;
}
runOnUiThread(new Runnable() {
@Override
public void run() {
try {
FixedSubsamplingScaleImageView iv = new FixedSubsamplingScaleImageView(GalleryActivity.this);
iv.setInitCallback(new FixedSubsamplingScaleImageView.InitedCallback() {
@Override
public void onInit() {
runOnUiThread(new Runnable() {
@Override
public void run() {
tag.thumbnailView.setVisibility(View.GONE);
tag.loadingView.setVisibility(View.GONE);
}
});
}
});
iv.setImageFile(file.getAbsolutePath(), new FixedSubsamplingScaleImageView.FailedCallback() {
@Override
public void onFail() {
setWebView(tag, file);
}
});
if (tag.downloadingTask.isCancelled()) return;
tag.layout.setVisibility(View.VISIBLE);
tag.layout.addView(iv);
} catch (Throwable t) {
System.gc();
Logger.e(TAG, t);
if (tag.downloadingTask.isCancelled()) return;
setWebView(tag, file);
}
}
});
}
private void setGif(final GalleryItemViewTag tag, final File file) {
if (!settings.useNativeGif()) {
setWebView(tag, file);
return;
}
runOnUiThread(new Runnable() {
@Override
public void run() {
ImageView iv = Build.VERSION.SDK_INT < Build.VERSION_CODES.FROYO ?
new ImageView(GalleryActivity.this) : new TouchGifView(GalleryActivity.this);
try {
GifDrawable drawable = new GifDrawable(file);
iv.setTag(drawable);
iv.setImageDrawable(drawable);
} catch (Throwable e) {
System.gc();
Logger.e(TAG, "cannot init GifDrawable", e);
if (tag.downloadingTask.isCancelled()) return;
setWebView(tag, file);
return;
}
if (tag.downloadingTask.isCancelled()) return;
tag.thumbnailView.setVisibility(View.GONE);
tag.loadingView.setVisibility(View.GONE);
tag.layout.setVisibility(View.VISIBLE);
tag.layout.addView(iv);
}
});
}
private void setSvg(GalleryItemViewTag tag, File file) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) setWebView(tag, file); else setOtherFile(tag, file);
}
private void setVideo(final GalleryItemViewTag tag, final File file) {
runOnUiThread(new Runnable() {
@Override
public void run() {
setOnClickView(tag, getString(R.string.gallery_tap_to_play), new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!settings.useInternalVideoPlayer()) {
openExternal();
} else {
recycleTag(tag, false);
tag.thumbnailView.setVisibility(View.GONE);
View videoContainer = inflater.inflate(R.layout.gallery_videoplayer, tag.layout);
final VideoView videoView = (VideoView)videoContainer.findViewById(R.id.gallery_video_view);
final TextView durationView = (TextView)videoContainer.findViewById(R.id.gallery_video_duration);
videoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(final MediaPlayer mp) {
mp.setLooping(true);
durationView.setText("00:00 / " + formatMediaPlayerTime(mp.getDuration()));
tag.timer = new Timer();
tag.timer.schedule(new TimerTask() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
try {
durationView.setText(formatMediaPlayerTime(mp.getCurrentPosition()) + " / " +
formatMediaPlayerTime(mp.getDuration()));
} catch (Exception e) {
Logger.e(TAG, e);
tag.timer.cancel();
}
}
});
}
}, 1000, 1000);
videoView.start();
}
});
videoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
Logger.e(TAG, "(Video) Error code: " + what);
if (tag.timer != null) tag.timer.cancel();
showError(tag, getString(R.string.gallery_error_play));
return true;
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR) {
CompatibilityImpl.setVideoViewZOrderOnTop(videoView);
}
videoView.setVideoPath(file.getAbsolutePath());
}
}
});
}
});
}
private void setAudio(final GalleryItemViewTag tag, final File file) {
runOnUiThread(new Runnable() {
@Override
public void run() {
setOnClickView(tag, getString(R.string.gallery_tap_to_play), new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!settings.useInternalAudioPlayer()) {
openExternal();
} else {
recycleTag(tag, false);
final TextView durationView = new TextView(GalleryActivity.this);
durationView.setGravity(Gravity.CENTER);
tag.layout.setVisibility(View.VISIBLE);
tag.layout.addView(durationView);
tag.audioPlayer = new MediaPlayer();
tag.audioPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(final MediaPlayer mp) {
mp.setLooping(true);
durationView.setText(getSpannedText("00:00 / " + formatMediaPlayerTime(mp.getDuration())));
tag.timer = new Timer();
tag.timer.schedule(new TimerTask() {
@Override
public void run() {
runOnUiThread(new Runnable() {
@Override
public void run() {
try {
durationView.setText(getSpannedText(formatMediaPlayerTime(mp.getCurrentPosition()) + " / " +
formatMediaPlayerTime(mp.getDuration())));
} catch (Exception e) {
Logger.e(TAG, e);
tag.timer.cancel();
}
}
});
}
}, 1000, 1000);
mp.start();
}
});
tag.audioPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
Logger.e(TAG, "(Audio) Error code: " + what);
if (tag.timer != null) tag.timer.cancel();
showError(tag, getString(R.string.gallery_error_play));
return true;
}
});
try {
tag.audioPlayer.setDataSource(file.getAbsolutePath());
tag.audioPlayer.prepareAsync();
} catch (Exception e) {
Logger.e(TAG, "audio player error", e);
if (tag.timer != null) tag.timer.cancel();
showError(tag, getString(R.string.gallery_error_play));
}
}
}
});
}
});
}
private String formatMediaPlayerTime(int milliseconds) {
int seconds = milliseconds / 1000 % 60;
int minutes = milliseconds / 60000;
return String.format(Locale.US, "%02d:%02d", minutes, seconds);
}
private void setOtherFile(final GalleryItemViewTag tag, final File file) {
runOnUiThread(new Runnable() {
@Override
public void run() {
setOnClickView(tag, getString(R.string.gallery_tap_to_open), new View.OnClickListener() {
@Override
public void onClick(View v) {
openExternal();
}
});
}
});
}
private void setExternalLink(final GalleryItemViewTag tag) {
runOnUiThread(new Runnable() {
@Override
public void run() {
int stringResId = R.string.gallery_tap_to_external_link;
try {
if (settings.doNotDownloadVideos() && tag.attachmentModel.type == AttachmentModel.TYPE_VIDEO)
stringResId = R.string.gallery_tap_to_play;
} catch (Exception e) {}
setOnClickView(tag, getString(stringResId), new View.OnClickListener() {
@Override
public void onClick(View v) {
openBrowser();
}
});
}
});
}
private void setOnClickView(GalleryItemViewTag tag, String message, View.OnClickListener handler) {
tag.thumbnailView.setVisibility(View.VISIBLE);
tag.loadingView.setVisibility(View.GONE);
TextView v = new TextView(GalleryActivity.this);
v.setGravity(Gravity.CENTER);
v.setText(getSpannedText(message));
tag.layout.setVisibility(View.VISIBLE);
tag.layout.addView(v);
v.setOnClickListener(handler);
}
private Spanned getSpannedText(String message) {
message = " " + message + " ";
SpannableStringBuilder spanned = new SpannableStringBuilder(message);
for (Object span : new Object[] { new ForegroundColorSpan(Color.WHITE), new BackgroundColorSpan(Color.parseColor("#88000000")) }) {
spanned.setSpan(span, 0, message.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return spanned;
}
private void setWebView(final GalleryItemViewTag tag, final File file) {
runOnUiThread(new Runnable() {
private boolean oomFlag = false;
private final ViewGroup.LayoutParams MATCH_PARAMS =
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
private void prepareWebView(WebView webView) {
webView.setBackgroundColor(Color.TRANSPARENT);
webView.setInitialScale(100);
webView.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR) {
CompatibilityImpl.setScrollbarFadingEnabled(webView, true);
}
WebSettings settings = webView.getSettings();
settings.setBuiltInZoomControls(true);
settings.setSupportZoom(true);
settings.setAllowFileAccess(true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR_MR1) {
CompatibilityImpl.setDefaultZoomFAR(settings);
CompatibilityImpl.setLoadWithOverviewMode(settings, true);
}
settings.setUseWideViewPort(true);
settings.setCacheMode(WebSettings.LOAD_NO_CACHE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
CompatibilityImpl.setBlockNetworkLoads(settings, true);
}
setScaleWebView(webView);
}
private void setScaleWebView(final WebView webView) {
Runnable callSetScaleWebView = new Runnable() {
@Override
public void run() {
setPrivateScaleWebView(webView);
}
};
Point resolution = new Point(tag.layout.getWidth(), tag.layout.getHeight());
if (resolution.equals(0, 0)) {
// wait until the view is measured and its size is known
AppearanceUtils.callWhenLoaded(tag.layout, callSetScaleWebView);
} else {
callSetScaleWebView.run();
}
}
private void setPrivateScaleWebView(WebView webView) {
Point imageSize = getImageSize(file);
Point resolution = new Point(tag.layout.getWidth(), tag.layout.getHeight());
//Logger.d(TAG, "Resolution: "+resolution.x+"x"+resolution.y);
double scaleX = (double)resolution.x / (double)imageSize.x;
double scaleY = (double)resolution.y / (double)imageSize.y;
int scale = (int)Math.round(Math.min(scaleX, scaleY) * 100d);
scale = Math.max(scale, 1);
//Logger.d(TAG, "Scale: "+(Math.min(scaleX, scaleY) * 100d));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR_MR1) {
double picdpi = (getResources().getDisplayMetrics().density * 160d) / scaleX;
if (picdpi >= 240) {
CompatibilityImpl.setDefaultZoomFAR(webView.getSettings());
} else if (picdpi <= 120) {
CompatibilityImpl.setDefaultZoomCLOSE(webView.getSettings());
} else {
CompatibilityImpl.setDefaultZoomMEDIUM(webView.getSettings());
}
}
webView.setInitialScale(scale);
webView.setPadding(0, 0, 0, 0);
}
private Point getImageSize(File file) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
return new Point(options.outWidth, options.outHeight);
}
private boolean useFallback(File file) {
String path = file.getPath().toLowerCase(Locale.US);
if (path.endsWith(".png")) return false;
if (path.endsWith(".jpg")) return false;
if (path.endsWith(".gif")) return false;
if (path.endsWith(".jpeg")) return false;
if (path.endsWith(".webp") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) return false;
return true;
}
@Override
public void run() {
try {
recycleTag(tag, false);
WebView webView = new WebViewFixed(GalleryActivity.this);
webView.setLayoutParams(MATCH_PARAMS);
tag.layout.addView(webView);
if (settings.fallbackWebView() || useFallback(file)) {
prepareWebView(webView);
webView.loadUrl(Uri.fromFile(file).toString());
} else {
JSWebView.setImage(webView, file);
}
tag.thumbnailView.setVisibility(View.GONE);
tag.loadingView.setVisibility(View.GONE);
tag.layout.setVisibility(View.VISIBLE);
} catch (OutOfMemoryError oom) {
System.gc();
Logger.e(TAG, oom);
if (!oomFlag) {
oomFlag = true;
run();
} else showError(tag, getString(R.string.error_out_of_memory));
}
}
});
}
public static interface FullscreenCallback {
void showUI(boolean hideAfterDelay);
void keepUI(boolean hideAfterDelay);
}
private FullscreenCallback fullscreenCallback;
private GestureDetector fullscreenGestureDetector;
public void setFullscreenCallback(FullscreenCallback fullscreenCallback) {
if (fullscreenGestureDetector == null) {
fullscreenGestureDetector = new GestureDetector(this, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
FullscreenCallback fullscreenCallback = GalleryActivity.this.fullscreenCallback;
if (fullscreenCallback != null) fullscreenCallback.showUI(true);
return true;
}
});
}
this.fullscreenCallback = fullscreenCallback;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (fullscreenCallback != null) {
fullscreenCallback.keepUI(MotionEventCompat.getActionMasked(ev) == MotionEvent.ACTION_UP);
fullscreenGestureDetector.onTouchEvent(ev);
}
return super.dispatchTouchEvent(ev);
}
@Override
public void onPanelClosed(int featureId, Menu menu) {
if (fullscreenCallback != null) fullscreenCallback.showUI(true);
super.onPanelClosed(featureId, menu);
}
@Override
public boolean onMenuOpened(int featureId, Menu menu) {
if (fullscreenCallback != null) fullscreenCallback.showUI(false);
return super.onMenuOpened(featureId, menu);
}
private class GalleryItemViewTag {
public CancellableTask downloadingTask;
public Timer timer;
public MediaPlayer audioPlayer;
public AttachmentModel attachmentModel;
public String attachmentHash;
public File file;
public ImageView thumbnailView;
public FrameLayout layout;
public View errorView;
public TextView errorText;
public View loadingView;
}
}