nya.miku.wishmaster.ui.gallery.GalleryActivity.java Source code

Java tutorial

Introduction

Here is the source code for nya.miku.wishmaster.ui.gallery.GalleryActivity.java

Source

/*
 * 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;
    }

}