org.mariotaku.twidere.activity.MediaViewerActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.mariotaku.twidere.activity.MediaViewerActivity.java

Source

/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.mariotaku.twidere.activity;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.ImageButton;
import android.widget.MediaController;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import com.afollestad.appthemeengine.Config;
import com.afollestad.appthemeengine.customizers.ATEToolbarCustomizer;
import com.commonsware.cwac.layouts.AspectLockedFrameLayout;
import com.davemorrissey.labs.subscaleview.ImageSource;
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageDecoder;
import com.sprylab.android.widget.TextureVideoView;

import org.apache.commons.lang3.ArrayUtils;
import org.mariotaku.mediaviewer.library.CacheDownloadLoader;
import org.mariotaku.mediaviewer.library.CacheDownloadMediaViewerFragment;
import org.mariotaku.mediaviewer.library.FileCache;
import org.mariotaku.mediaviewer.library.IMediaViewerActivity;
import org.mariotaku.mediaviewer.library.MediaDownloader;
import org.mariotaku.mediaviewer.library.MediaViewerFragment;
import org.mariotaku.mediaviewer.library.subsampleimageview.SubsampleImageViewerFragment;
import org.mariotaku.twidere.R;
import org.mariotaku.twidere.activity.iface.IExtendedActivity;
import org.mariotaku.twidere.fragment.ProgressDialogFragment;
import org.mariotaku.twidere.model.ParcelableMedia;
import org.mariotaku.twidere.model.ParcelableStatus;
import org.mariotaku.twidere.model.UserKey;
import org.mariotaku.twidere.provider.CacheProvider;
import org.mariotaku.twidere.provider.ShareProvider;
import org.mariotaku.twidere.task.SaveFileTask;
import org.mariotaku.twidere.task.SaveMediaToGalleryTask;
import org.mariotaku.twidere.util.AsyncTaskUtils;
import org.mariotaku.twidere.util.IntentUtils;
import org.mariotaku.twidere.util.MenuUtils;
import org.mariotaku.twidere.util.PermissionUtils;
import org.mariotaku.twidere.util.TwidereMathUtils;
import org.mariotaku.twidere.util.UriUtils;
import org.mariotaku.twidere.util.Utils;
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper;
import org.mariotaku.twidere.util.media.MediaExtra;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.concurrent.TimeUnit;

import javax.inject.Inject;

import edu.tsinghua.hotmobi.HotMobiLogger;
import edu.tsinghua.hotmobi.model.MediaDownloadEvent;
import pl.droidsonroids.gif.GifTextureView;
import pl.droidsonroids.gif.InputSource;

public final class MediaViewerActivity extends BaseActivity
        implements IExtendedActivity, ATEToolbarCustomizer, IMediaViewerActivity {

    private static final int REQUEST_SHARE_MEDIA = 201;
    private static final int REQUEST_PERMISSION_SAVE_MEDIA = 202;
    private static final int REQUEST_PERMISSION_SHARE_MEDIA = 203;

    @Inject
    FileCache mFileCache;
    @Inject
    MediaDownloader mMediaDownloader;

    private ParcelableMedia[] mMedia;
    private int mSaveToStoragePosition = -1;
    private int mShareMediaPosition = -1;

    private Helper mHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        GeneralComponentHelper.build(this).inject(this);
        mHelper = new Helper(this);
        mHelper.onCreate(savedInstanceState);
        ActionBar actionBar = getSupportActionBar();
        assert actionBar != null;
        actionBar.setDisplayHomeAsUpEnabled(true);
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
        case REQUEST_SHARE_MEDIA: {
            ShareProvider.clearTempFiles(this);
            break;
        }
        }
    }

    @Override
    public void onContentChanged() {
        super.onContentChanged();
        mHelper.onContentChanged();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_media_viewer, menu);
        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        final ViewPager viewPager = findViewPager();
        final PagerAdapter adapter = viewPager.getAdapter();
        final int currentItem = viewPager.getCurrentItem();
        if (currentItem < 0 || currentItem >= adapter.getCount())
            return false;
        final Object object = adapter.instantiateItem(viewPager, currentItem);
        if (!(object instanceof MediaViewerFragment))
            return false;
        if (object instanceof CacheDownloadMediaViewerFragment) {
            CacheDownloadMediaViewerFragment f = (CacheDownloadMediaViewerFragment) object;
            final boolean running = f.getLoaderManager().hasRunningLoaders();
            final boolean downloaded = f.hasDownloadedData();
            MenuUtils.setMenuItemAvailability(menu, R.id.refresh, !running && !downloaded);
            MenuUtils.setMenuItemAvailability(menu, R.id.share, !running && downloaded);
            MenuUtils.setMenuItemAvailability(menu, R.id.save, !running && downloaded);
        } else {
            MenuUtils.setMenuItemAvailability(menu, R.id.refresh, false);
            MenuUtils.setMenuItemAvailability(menu, R.id.share, true);
            MenuUtils.setMenuItemAvailability(menu, R.id.save, false);
        }
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        final ViewPager viewPager = findViewPager();
        final PagerAdapter adapter = viewPager.getAdapter();
        final int currentItem = viewPager.getCurrentItem();
        if (currentItem < 0 || currentItem >= adapter.getCount())
            return false;
        final Object object = adapter.instantiateItem(viewPager, currentItem);
        if (!(object instanceof MediaViewerFragment))
            return false;
        switch (item.getItemId()) {
        case R.id.refresh: {
            if (object instanceof CacheDownloadMediaViewerFragment) {
                final CacheDownloadMediaViewerFragment fragment = (CacheDownloadMediaViewerFragment) object;
                fragment.startLoading(true);
                fragment.showProgress(true, 0);
                fragment.setMediaViewVisible(false);
            }
            return true;
        }
        case R.id.share: {
            if (object instanceof CacheDownloadMediaViewerFragment) {
                requestAndShareMedia(currentItem);
            } else {
                final ParcelableMedia media = getMedia()[currentItem];
                final Intent intent = new Intent(Intent.ACTION_SEND);
                intent.setType("text/plain");
                intent.putExtra(Intent.EXTRA_TEXT, media.url);
                startActivity(Intent.createChooser(intent, getString(R.string.share)));
            }
            return true;
        }
        case R.id.save: {
            requestAndSaveToStorage(currentItem);
            return true;
        }
        case R.id.open_in_browser: {
            final ParcelableMedia media = getMedia()[currentItem];
            try {
                final Uri uri = Uri.parse(media.url);
                final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                intent.addCategory(Intent.CATEGORY_BROWSABLE);
                intent.setPackage(IntentUtils.getDefaultBrowserPackage(this, uri, true));
                startActivity(intent);
            } catch (ActivityNotFoundException e) {
                // TODO show error, or improve app url
            }
            return true;
        }
        case android.R.id.home: {
            finish();
            return true;
        }
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
            @NonNull int[] grantResults) {
        switch (requestCode) {
        case REQUEST_PERMISSION_SAVE_MEDIA: {
            if (PermissionUtils.hasPermission(permissions, grantResults,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                saveToStorage();
            } else {
                Toast.makeText(this, R.string.save_media_no_storage_permission_message, Toast.LENGTH_LONG).show();
            }
            return;
        }
        case REQUEST_PERMISSION_SHARE_MEDIA: {
            if (!PermissionUtils.hasPermission(permissions, grantResults,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                Toast.makeText(this, R.string.share_media_no_storage_permission_message, Toast.LENGTH_LONG).show();
            }
            shareMedia();
            return;
        }
        }
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

    @Override
    public void toggleBar() {
        setBarVisibility(!isBarShowing());
    }

    @Override
    @Nullable
    public String getATEKey() {
        return VALUE_THEME_NAME_DARK;
    }

    @Override
    public int getInitialPosition() {
        return ArrayUtils.indexOf(getMedia(), getInitialMedia());
    }

    @Override
    public int getLayoutRes() {
        return R.layout.activity_media_viewer;
    }

    @Override
    @NonNull
    public ViewPager findViewPager() {
        final ViewPager viewPager = (ViewPager) findViewById(R.id.view_pager);
        if (viewPager == null)
            throw new NullPointerException();
        return viewPager;
    }

    @Override
    public boolean isBarShowing() {
        final ActionBar actionBar = getSupportActionBar();
        return actionBar != null && actionBar.isShowing();
    }

    @Override
    public void setBarVisibility(boolean visible) {
        final ActionBar actionBar = getSupportActionBar();
        if (actionBar == null)
            return;
        if (visible) {
            actionBar.show();
        } else {
            actionBar.hide();
        }
    }

    @Override
    public MediaDownloader getDownloader() {
        return mMediaDownloader;
    }

    @Override
    public FileCache getFileCache() {
        return mFileCache;
    }

    @SuppressLint("SwitchIntDef")
    @Override
    public MediaViewerFragment instantiateMediaFragment(int position) {
        final ParcelableMedia media = getMedia()[position];
        final Bundle args = new Bundle();
        final Intent intent = getIntent();
        args.putParcelable(EXTRA_ACCOUNT_KEY, intent.getParcelableExtra(EXTRA_ACCOUNT_KEY));
        args.putParcelable(EXTRA_MEDIA, media);
        args.putParcelable(EXTRA_STATUS, intent.getParcelableExtra(EXTRA_STATUS));
        switch (media.type) {
        case ParcelableMedia.Type.IMAGE: {
            if (media.media_url == null) {
                return (MediaViewerFragment) Fragment.instantiate(this, ExternalBrowserPageFragment.class.getName(),
                        args);
            }
            args.putParcelable(ImagePageFragment.EXTRA_MEDIA_URI, Uri.parse(media.media_url));
            if (media.media_url.endsWith(".gif")) {
                return (MediaViewerFragment) Fragment.instantiate(this, GifPageFragment.class.getName(), args);
            } else {
                return (MediaViewerFragment) Fragment.instantiate(this, ImagePageFragment.class.getName(), args);
            }
        }
        case ParcelableMedia.Type.ANIMATED_GIF:
        case ParcelableMedia.Type.CARD_ANIMATED_GIF: {
            args.putBoolean(VideoPageFragment.EXTRA_LOOP, true);
            return (MediaViewerFragment) Fragment.instantiate(this, VideoPageFragment.class.getName(), args);
        }
        case ParcelableMedia.Type.VIDEO: {
            return (MediaViewerFragment) Fragment.instantiate(this, VideoPageFragment.class.getName(), args);
        }
        case ParcelableMedia.Type.EXTERNAL_PLAYER: {
            return (MediaViewerFragment) Fragment.instantiate(this, ExternalBrowserPageFragment.class.getName(),
                    args);
        }
        }
        throw new UnsupportedOperationException(String.valueOf(media));
    }

    @Override
    public int getMediaCount() {
        return getMedia().length;
    }

    @Override
    public int getLightToolbarMode(@Nullable Toolbar toolbar) {
        return Config.LIGHT_TOOLBAR_OFF;
    }

    @Override
    public int getToolbarColor(@Nullable Toolbar toolbar) {
        return 0;
    }

    public boolean hasStatus() {
        return getIntent().hasExtra(EXTRA_STATUS);
    }

    private ParcelableStatus getStatus() {
        return getIntent().getParcelableExtra(EXTRA_STATUS);
    }

    private ParcelableMedia getInitialMedia() {
        return getIntent().getParcelableExtra(EXTRA_CURRENT_MEDIA);
    }

    private ParcelableMedia[] getMedia() {
        if (mMedia != null)
            return mMedia;
        return mMedia = Utils.newParcelableArray(getIntent().getParcelableArrayExtra(EXTRA_MEDIA),
                ParcelableMedia.CREATOR);
    }

    protected void processShareIntent(Intent intent) {
        if (!hasStatus())
            return;
        final ParcelableStatus status = getStatus();
        intent.putExtra(Intent.EXTRA_SUBJECT, IntentUtils.getStatusShareSubject(this, status));
        intent.putExtra(Intent.EXTRA_TEXT, IntentUtils.getStatusShareText(this, status));
    }

    protected void requestAndSaveToStorage(int position) {
        mSaveToStoragePosition = position;
        if (PermissionUtils.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
            saveToStorage();
        } else {
            final String[] permissions;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                permissions = new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE,
                        Manifest.permission.READ_EXTERNAL_STORAGE };
            } else {
                permissions = new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE };
            }
            ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSION_SAVE_MEDIA);
        }
    }

    protected void requestAndShareMedia(int position) {
        mShareMediaPosition = position;
        if (PermissionUtils.hasPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
            shareMedia();
        } else {
            final String[] permissions;
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                permissions = new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE,
                        Manifest.permission.READ_EXTERNAL_STORAGE };
            } else {
                permissions = new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE };
            }
            ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSION_SHARE_MEDIA);
        }
    }

    protected void shareMedia() {
        if (mShareMediaPosition == -1)
            return;
        final ViewPager viewPager = findViewPager();
        final PagerAdapter adapter = viewPager.getAdapter();
        final Object object = adapter.instantiateItem(viewPager, mShareMediaPosition);
        if (!(object instanceof CacheDownloadMediaViewerFragment))
            return;
        CacheDownloadLoader.Result result = ((CacheDownloadMediaViewerFragment) object).getDownloadResult();
        if (result == null || result.cacheUri == null) {
            // TODO show error
            return;
        }
        final File destination = ShareProvider.getFilesDir(this);
        if (destination == null)
            return;
        String type;
        if (object instanceof VideoPageFragment) {
            type = CacheProvider.Type.VIDEO;
        } else if (object instanceof ImagePageFragment) {
            type = CacheProvider.Type.IMAGE;
        } else if (object instanceof GifPageFragment) {
            type = CacheProvider.Type.IMAGE;
        } else {
            throw new UnsupportedOperationException("Unsupported fragment " + object);
        }
        final SaveFileTask task = new SaveFileTask(this, result.cacheUri, destination,
                new CacheProvider.CacheFileTypeCallback(this, type)) {
            private static final String PROGRESS_FRAGMENT_TAG = "progress";

            @Override
            protected void dismissProgress() {
                final IExtendedActivity activity = (IExtendedActivity) getContext();
                if (activity == null)
                    return;
                activity.executeAfterFragmentResumed(new IExtendedActivity.Action() {
                    @Override
                    public void execute(IExtendedActivity activity) {
                        final FragmentManager fm = ((FragmentActivity) activity).getSupportFragmentManager();
                        final DialogFragment fragment = (DialogFragment) fm
                                .findFragmentByTag(PROGRESS_FRAGMENT_TAG);
                        if (fragment != null) {
                            fragment.dismiss();
                        }
                    }
                });
            }

            @Override
            protected void showProgress() {
                final IExtendedActivity activity = (IExtendedActivity) getContext();
                if (activity == null)
                    return;
                activity.executeAfterFragmentResumed(new IExtendedActivity.Action() {
                    @Override
                    public void execute(IExtendedActivity activity) {
                        final DialogFragment fragment = new ProgressDialogFragment();
                        fragment.setCancelable(false);
                        fragment.show(((FragmentActivity) activity).getSupportFragmentManager(),
                                PROGRESS_FRAGMENT_TAG);
                    }
                });
            }

            @Override
            protected void onFileSaved(@NonNull File savedFile, @Nullable String mimeType) {
                final MediaViewerActivity activity = (MediaViewerActivity) getContext();
                if (activity == null)
                    return;

                final Uri fileUri = ShareProvider.getUriForFile(activity, AUTHORITY_TWIDERE_SHARE, savedFile);

                final Intent intent = new Intent(Intent.ACTION_SEND);
                intent.setDataAndType(fileUri, mimeType);
                intent.putExtra(Intent.EXTRA_STREAM, fileUri);
                intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                    intent.addFlags(Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
                }
                activity.processShareIntent(intent);
                startActivityForResult(Intent.createChooser(intent, activity.getString(R.string.share)),
                        REQUEST_SHARE_MEDIA);
            }

            @Override
            protected void onFileSaveFailed() {
                final MediaViewerActivity activity = (MediaViewerActivity) getContext();
                if (activity == null)
                    return;
                Toast.makeText(activity, R.string.error_occurred, Toast.LENGTH_SHORT).show();
            }
        };
        AsyncTaskUtils.executeTask(task);
    }

    protected void saveToStorage() {
        if (mSaveToStoragePosition == -1)
            return;
        final ViewPager viewPager = findViewPager();
        final PagerAdapter adapter = viewPager.getAdapter();
        final Object object = adapter.instantiateItem(viewPager, mSaveToStoragePosition);
        if (!(object instanceof CacheDownloadMediaViewerFragment))
            return;
        final CacheDownloadMediaViewerFragment f = (CacheDownloadMediaViewerFragment) object;
        final CacheDownloadLoader.Result result = f.getDownloadResult();
        if (result == null)
            return;
        final Uri cacheUri = result.cacheUri;
        final boolean hasMedia = cacheUri != null;
        if (!hasMedia)
            return;
        SaveFileTask task;
        if (f instanceof ImagePageFragment) {
            task = SaveMediaToGalleryTask.create(this, cacheUri, CacheProvider.Type.IMAGE);
        } else if (f instanceof VideoPageFragment) {
            task = SaveMediaToGalleryTask.create(this, cacheUri, CacheProvider.Type.VIDEO);
        } else if (f instanceof GifPageFragment) {
            task = SaveMediaToGalleryTask.create(this, cacheUri, CacheProvider.Type.IMAGE);
        } else {
            throw new UnsupportedOperationException();
        }
        AsyncTaskUtils.executeTask(task);
    }

    public static class ImagePageFragment extends SubsampleImageViewerFragment {
        private int mMediaLoadState;
        private MediaDownloadEvent mMediaDownloadEvent;
        private CacheDownloadLoader.ResultCreator mResultCreator;

        static Bitmap decodeBitmap(ContentResolver cr, Uri uri, BitmapFactory.Options o) throws IOException {
            InputStream is = null;
            try {
                is = cr.openInputStream(uri);
                return BitmapFactory.decodeStream(is, null, o);
            } finally {
                Utils.closeSilently(is);
            }
        }

        static Uri replaceTwitterMediaUri(Uri downloadUri) {
            //            String uriString = downloadUri.toString();
            //            if (TwitterMediaProvider.isSupported(uriString)) {
            //                final String suffix = ".jpg";
            //                int lastIndexOfJpegSuffix = uriString.lastIndexOf(suffix);
            //                if (lastIndexOfJpegSuffix == -1) return downloadUri;
            //                final int endOfSuffix = lastIndexOfJpegSuffix + suffix.length();
            //                if (endOfSuffix == uriString.length()) {
            //                    return Uri.parse(uriString.substring(0, lastIndexOfJpegSuffix) + ".png");
            //                } else {
            //                    // Seems :orig suffix won't work jpegs -> pngs
            //                    String sizeSuffix = uriString.substring(endOfSuffix);
            //                    if (":orig".equals(sizeSuffix)) {
            //                        sizeSuffix = ":large";
            //                    }
            //                    return Uri.parse(uriString.substring(0, lastIndexOfJpegSuffix) + ".png" +
            //                            sizeSuffix);
            //                }
            //            }
            return downloadUri;
        }

        @Override
        public void onActivityCreated(@Nullable Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
        }

        @Override
        public void setUserVisibleHint(boolean isVisibleToUser) {
            super.setUserVisibleHint(isVisibleToUser);
            final FragmentActivity activity = getActivity();
            if (isVisibleToUser && activity != null) {
                activity.supportInvalidateOptionsMenu();
            }
        }

        @Override
        protected Object getDownloadExtra() {
            final MediaExtra mediaExtra = new MediaExtra();
            mediaExtra.setAccountKey(getAccountKey());
            final Uri origDownloadUri = super.getDownloadUri();
            final Uri downloadUri = getDownloadUri();
            if (origDownloadUri != null && downloadUri != null) {
                final String fallbackUrl = origDownloadUri.toString();
                mediaExtra.setFallbackUrl(fallbackUrl);
                mediaExtra.setSkipUrlReplacing(!fallbackUrl.equals(downloadUri.toString()));
            }
            return mediaExtra;
        }

        @Nullable
        @Override
        protected Uri getDownloadUri() {
            final Uri downloadUri = super.getDownloadUri();
            if (downloadUri == null)
                return null;
            return replaceTwitterMediaUri(downloadUri);
        }

        @Override
        public boolean hasDownloadedData() {
            return super.hasDownloadedData() && mMediaLoadState != State.ERROR;
        }

        @Override
        protected void onMediaLoadStateChange(@State int state) {
            mMediaLoadState = state;
            final FragmentActivity activity = getActivity();
            if (getUserVisibleHint() && activity != null) {
                activity.supportInvalidateOptionsMenu();
            }
        }

        @Override
        protected void setupImageView(SubsamplingScaleImageView imageView) {
            imageView.setMaxScale(getResources().getDisplayMetrics().density);
            imageView.setBitmapDecoderClass(PreviewBitmapDecoder.class);
        }

        @NonNull
        @Override
        protected ImageSource getImageSource(@NonNull CacheDownloadLoader.Result data) {
            assert data.cacheUri != null;
            if (!(data instanceof SizedResult)) {
                return super.getImageSource(data);
            }
            final ImageSource imageSource = ImageSource.uri(data.cacheUri);
            imageSource.tilingEnabled();
            imageSource.dimensions(((SizedResult) data).getWidth(), ((SizedResult) data).getHeight());
            return imageSource;
        }

        @Override
        protected ImageSource getPreviewImageSource(@NonNull CacheDownloadLoader.Result data) {
            if (!(data instanceof SizedResult))
                return null;
            assert data.cacheUri != null;
            return ImageSource.uri(UriUtils.appendQueryParameters(data.cacheUri, QUERY_PARAM_PREVIEW, true));
        }

        @Nullable
        @Override
        protected CacheDownloadLoader.ResultCreator getResultCreator() {
            if (mResultCreator != null)
                return mResultCreator;
            return mResultCreator = new SizedResultCreator(getContext());
        }

        private ParcelableMedia getMedia() {
            return getArguments().getParcelable(EXTRA_MEDIA);
        }

        private UserKey getAccountKey() {
            return getArguments().getParcelable(EXTRA_ACCOUNT_KEY);
        }

        @Override
        public void onDownloadRequested(long nonce) {
            super.onDownloadRequested(nonce);
            final Context context = getContext();
            if (context != null) {
                mMediaDownloadEvent = MediaDownloadEvent.create(context, getMedia(), nonce);
            } else {
                mMediaDownloadEvent = null;
            }
        }

        @Override
        public void onDownloadStart(long total, long nonce) {
            super.onDownloadStart(total, nonce);
            if (mMediaDownloadEvent != null && mMediaDownloadEvent.getNonce() == nonce) {
                mMediaDownloadEvent.setOpenedTime(System.currentTimeMillis());
                mMediaDownloadEvent.setSize(total);
            }
        }

        @Override
        public void onDownloadFinished(long nonce) {
            super.onDownloadFinished(nonce);
            if (mMediaDownloadEvent != null && mMediaDownloadEvent.getNonce() == nonce) {
                mMediaDownloadEvent.markEnd();
                HotMobiLogger.getInstance(getContext()).log(getAccountKey(), mMediaDownloadEvent);
                mMediaDownloadEvent = null;
            }
        }

        static class SizedResult extends CacheDownloadLoader.Result {

            private final int width, height;

            public SizedResult(@NonNull Uri cacheUri, int width, int height) {
                super(cacheUri, null);
                this.width = width;
                this.height = height;
            }

            public int getWidth() {
                return width;
            }

            public int getHeight() {
                return height;
            }
        }

        static class SizedResultCreator implements CacheDownloadLoader.ResultCreator {

            private final Context context;

            SizedResultCreator(Context context) {
                this.context = context;
            }

            @Override
            public CacheDownloadLoader.Result create(Uri uri) {
                BitmapFactory.Options o = new BitmapFactory.Options();
                o.inJustDecodeBounds = true;
                try {
                    decodeBitmap(context.getContentResolver(), uri, o);
                } catch (IOException e) {
                    return CacheDownloadLoader.Result.getInstance(uri);
                }
                if (o.outWidth > 0 && o.outHeight > 0) {
                    return new SizedResult(uri, o.outWidth, o.outHeight);
                }
                return CacheDownloadLoader.Result.getInstance(uri);
            }

        }

        public static class PreviewBitmapDecoder extends SkiaImageDecoder {
            @Override
            public Bitmap decode(Context context, Uri uri) throws Exception {
                if (AUTHORITY_TWIDERE_CACHE.equals(uri.getAuthority())
                        && uri.getBooleanQueryParameter(QUERY_PARAM_PREVIEW, false)) {
                    BitmapFactory.Options o = new BitmapFactory.Options();
                    o.inJustDecodeBounds = true;
                    o.inPreferredConfig = Bitmap.Config.RGB_565;
                    final ContentResolver cr = context.getContentResolver();
                    decodeBitmap(cr, uri, o);
                    final DisplayMetrics dm = context.getResources().getDisplayMetrics();
                    final int targetSize = Math.min(1024, Math.max(dm.widthPixels, dm.heightPixels));
                    final double sizeRatio = Math.ceil(Math.max(o.outHeight, o.outWidth) / (double) targetSize);
                    o.inSampleSize = TwidereMathUtils.nextPowerOf2((int) Math.max(1, sizeRatio));
                    o.inJustDecodeBounds = false;
                    final Bitmap bitmap = decodeBitmap(cr, uri, o);
                    if (bitmap == null)
                        throw new IOException();
                    return bitmap;
                }
                return super.decode(context, uri);
            }

        }
    }

    public static class GifPageFragment extends CacheDownloadMediaViewerFragment {

        private GifTextureView mGifView;
        private MediaDownloadEvent mMediaDownloadEvent;

        @Override
        public void onActivityCreated(@Nullable Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
            startLoading(false);
        }

        @Nullable
        @Override
        protected Uri getDownloadUri() {
            return getArguments().getParcelable(ImagePageFragment.EXTRA_MEDIA_URI);
        }

        @Nullable
        @Override
        protected Object getDownloadExtra() {
            return null;
        }

        @Override
        protected void displayMedia(CacheDownloadLoader.Result result) {
            final Context context = getContext();
            if (context == null)
                return;
            if (result.cacheUri != null) {
                mGifView.setInputSource(new InputSource.UriSource(context.getContentResolver(), result.cacheUri));
            } else {
                mGifView.setInputSource(null);
            }
        }

        @Override
        protected boolean isAbleToLoad() {
            return getDownloadUri() != null;
        }

        @Override
        protected View onCreateMediaView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
            return inflater.inflate(R.layout.layout_media_viewer_gif, parent, false);
        }

        @Override
        public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);
            mGifView = (GifTextureView) view.findViewById(R.id.gif_view);
            mGifView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    ((MediaViewerActivity) getActivity()).toggleBar();
                }
            });
        }

        @Override
        protected void recycleMedia() {
            mGifView.setInputSource(null);
        }

        private ParcelableMedia getMedia() {
            return getArguments().getParcelable(EXTRA_MEDIA);
        }

        private UserKey getAccountKey() {
            return getArguments().getParcelable(EXTRA_ACCOUNT_KEY);
        }

        @Override
        public void onDownloadRequested(long nonce) {
            super.onDownloadRequested(nonce);
            final Context context = getContext();
            if (context != null) {
                mMediaDownloadEvent = MediaDownloadEvent.create(context, getMedia(), nonce);
            } else {
                mMediaDownloadEvent = null;
            }
        }

        @Override
        public void onDownloadStart(long total, long nonce) {
            super.onDownloadStart(total, nonce);
            if (mMediaDownloadEvent != null && mMediaDownloadEvent.getNonce() == nonce) {
                mMediaDownloadEvent.setOpenedTime(System.currentTimeMillis());
                mMediaDownloadEvent.setSize(total);
            }
        }

        @Override
        public void onDownloadFinished(long nonce) {
            super.onDownloadFinished(nonce);
            if (mMediaDownloadEvent != null && mMediaDownloadEvent.getNonce() == nonce) {
                mMediaDownloadEvent.markEnd();
                HotMobiLogger.getInstance(getContext()).log(getAccountKey(), mMediaDownloadEvent);
                mMediaDownloadEvent = null;
            }
        }
    }

    public static class VideoPageFragment extends CacheDownloadMediaViewerFragment
            implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener,
            MediaPlayer.OnCompletionListener, View.OnClickListener {

        private static final String EXTRA_LOOP = "loop";
        private static final String[] SUPPORTED_VIDEO_TYPES;
        private static final String[] FALLBACK_VIDEO_TYPES;

        static {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                SUPPORTED_VIDEO_TYPES = new String[] { "video/mp4" };
            } else {
                SUPPORTED_VIDEO_TYPES = new String[] { "video/webm", "video/mp4" };
            }
            FALLBACK_VIDEO_TYPES = new String[] { "video/mp4" };
        }

        private TextureVideoView mVideoView;
        private View mVideoViewOverlay;
        private ProgressBar mVideoViewProgress;
        private TextView mDurationLabel, mPositionLabel;
        private ImageButton mPlayPauseButton, mVolumeButton;
        private View mVideoControl;
        private boolean mPlayAudio;
        private VideoPlayProgressRunnable mVideoProgressRunnable;
        private MediaPlayer mMediaPlayer;
        private int mMediaPlayerError;
        private MediaDownloadEvent mMediaDownloadEvent;

        @Override
        protected Object getDownloadExtra() {
            final MediaExtra extra = new MediaExtra();
            extra.setUseThumbor(false);
            final Pair<String, String> fallbackUrlAndType = getBestVideoUrlAndType(getMedia(),
                    FALLBACK_VIDEO_TYPES);
            if (fallbackUrlAndType != null) {
                extra.setFallbackUrl(fallbackUrlAndType.first);
            }
            return extra;
        }

        public boolean isLoopEnabled() {
            return getArguments().getBoolean(EXTRA_LOOP, false);
        }

        @Override
        protected boolean isAbleToLoad() {
            return getDownloadUri() != null;
        }

        @Nullable
        @Override
        protected Uri getDownloadUri() {
            final Pair<String, String> bestVideoUrlAndType = getBestVideoUrlAndType(getMedia(),
                    SUPPORTED_VIDEO_TYPES);
            if (bestVideoUrlAndType != null && bestVideoUrlAndType.first != null) {
                return Uri.parse(bestVideoUrlAndType.first);
            }
            return getArguments().getParcelable(ImagePageFragment.EXTRA_MEDIA_URI);
        }

        @Override
        protected void displayMedia(CacheDownloadLoader.Result result) {
            mVideoView.setVideoURI(result.cacheUri);
            mVideoControl.setVisibility(View.GONE);
            setMediaViewVisible(true);
            final FragmentActivity activity = getActivity();
            if (activity != null) {
                activity.supportInvalidateOptionsMenu();
            }
        }

        @Override
        protected void recycleMedia() {

        }

        @Override
        public void onStart() {
            super.onStart();
        }

        @Override
        public void onStop() {
            super.onStop();
        }

        @Override
        public void onCompletion(MediaPlayer mp) {
            updatePlayerState();
            //            mVideoViewProgress.removeCallbacks(mVideoProgressRunnable);
            //            mVideoViewProgress.setVisibility(View.GONE);
        }

        @Override
        public boolean onError(MediaPlayer mp, int what, int extra) {
            mMediaPlayer = null;
            mVideoViewProgress.removeCallbacks(mVideoProgressRunnable);
            mVideoViewProgress.setVisibility(View.GONE);
            mVideoControl.setVisibility(View.GONE);
            mMediaPlayerError = what;
            return true;
        }

        @Override
        public void onPrepared(MediaPlayer mp) {
            if (getUserVisibleHint()) {
                mMediaPlayer = mp;
                mMediaPlayerError = 0;
                mp.setScreenOnWhilePlaying(true);
                updateVolume();
                mp.setLooping(isLoopEnabled());
                mp.start();
                mVideoViewProgress.setVisibility(View.VISIBLE);
                mVideoViewProgress.post(mVideoProgressRunnable);
                updatePlayerState();
                mVideoControl.setVisibility(View.VISIBLE);
            }
        }

        private void updateVolume() {

            final ImageButton b = mVolumeButton;
            if (b != null) {
                b.setImageResource(
                        mPlayAudio ? R.drawable.ic_action_speaker_max : R.drawable.ic_action_speaker_muted);
            }
            final MediaPlayer mp = mMediaPlayer;
            if (mp == null)
                return;
            try {
                if (mPlayAudio) {
                    mp.setVolume(1, 1);
                } else {
                    mp.setVolume(0, 0);
                }
            } catch (IllegalStateException e) {
                // Ignore
            }
        }

        @Override
        public void onViewCreated(View view, Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);
            mVideoView = (TextureVideoView) view.findViewById(R.id.video_view);
            mVideoViewOverlay = view.findViewById(R.id.video_view_overlay);
            mVideoViewProgress = (ProgressBar) view.findViewById(R.id.video_view_progress);
            mDurationLabel = (TextView) view.findViewById(R.id.duration_label);
            mPositionLabel = (TextView) view.findViewById(R.id.position_label);
            mPlayPauseButton = (ImageButton) view.findViewById(R.id.play_pause_button);
            mVolumeButton = (ImageButton) view.findViewById(R.id.volume_button);
            mVideoControl = view.findViewById(R.id.video_control);

        }

        @Override
        public void setUserVisibleHint(boolean isVisibleToUser) {
            super.setUserVisibleHint(isVisibleToUser);
            if (isVisibleToUser) {
                final FragmentActivity activity = getActivity();
                if (activity != null) {
                    activity.supportInvalidateOptionsMenu();
                }
            } else if (mVideoView != null && mVideoView.isPlaying()) {
                mVideoView.pause();
                updatePlayerState();
            }
        }

        @Override
        public void onActivityCreated(@Nullable Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
            setHasOptionsMenu(true);

            Handler handler = mVideoViewProgress.getHandler();
            if (handler == null) {
                handler = new Handler(getActivity().getMainLooper());
            }

            AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);

            // Play audio by default if ringer mode on
            mPlayAudio = am.getRingerMode() == AudioManager.RINGER_MODE_NORMAL;

            mVideoProgressRunnable = new VideoPlayProgressRunnable(handler, mVideoViewProgress, mDurationLabel,
                    mPositionLabel, mVideoView);

            mVideoViewOverlay.setOnClickListener(this);
            mVideoView.setOnPreparedListener(this);
            mVideoView.setOnErrorListener(this);
            mVideoView.setOnCompletionListener(this);

            mPlayPauseButton.setOnClickListener(this);
            mVolumeButton.setOnClickListener(this);
            mVideoControl.setVisibility(View.GONE);
            startLoading(false);
            setMediaViewVisible(false);
            updateVolume();
        }

        @SuppressLint("SwitchIntDef")
        @Nullable
        private Pair<String, String> getBestVideoUrlAndType(@Nullable final ParcelableMedia media,
                @NonNull final String[] supportedTypes) {
            if (media == null)
                return null;
            switch (media.type) {
            case ParcelableMedia.Type.VIDEO:
            case ParcelableMedia.Type.ANIMATED_GIF: {
                if (media.video_info == null) {
                    return Pair.create(media.media_url, null);
                }
                for (String supportedType : supportedTypes) {
                    for (ParcelableMedia.VideoInfo.Variant variant : media.video_info.variants) {
                        if (supportedType.equalsIgnoreCase(variant.content_type))
                            return Pair.create(variant.url, variant.content_type);
                    }
                }
                return null;
            }
            case ParcelableMedia.Type.CARD_ANIMATED_GIF: {
                return Pair.create(media.media_url, "video/mp4");
            }
            default: {
                return null;
            }
            }
        }

        @Override
        public void onClick(View v) {
            switch (v.getId()) {
            case R.id.volume_button: {
                mPlayAudio = !mPlayAudio;
                updateVolume();
                break;
            }
            case R.id.play_pause_button: {
                final MediaPlayer mp = mMediaPlayer;
                if (mp != null) {
                    if (mp.isPlaying()) {
                        mp.pause();
                    } else {
                        mp.start();
                    }
                }
                updatePlayerState();
                break;
            }
            case R.id.video_view_overlay: {
                final MediaViewerActivity activity = (MediaViewerActivity) getActivity();
                if (mVideoControl.getVisibility() == View.VISIBLE) {
                    mVideoControl.setVisibility(View.GONE);
                    activity.setBarVisibility(false);
                } else {
                    mVideoControl.setVisibility(View.VISIBLE);
                    activity.setBarVisibility(true);
                }
                break;
            }
            }
        }

        private void updatePlayerState() {
            final MediaPlayer mp = mMediaPlayer;
            if (mp != null) {
                final boolean playing = mp.isPlaying();
                mPlayPauseButton.setContentDescription(getString(playing ? R.string.pause : R.string.play));
                mPlayPauseButton
                        .setImageResource(playing ? R.drawable.ic_action_pause : R.drawable.ic_action_play_arrow);
            } else {
                mPlayPauseButton.setContentDescription(getString(R.string.play));
                mPlayPauseButton.setImageResource(R.drawable.ic_action_play_arrow);
            }
        }

        @Override
        public View onCreateMediaView(LayoutInflater inflater, @Nullable ViewGroup container,
                @Nullable Bundle savedInstanceState) {
            return inflater.inflate(R.layout.layout_media_viewer_texture_video_view, container, false);
        }

        @Override
        public void onDownloadRequested(long nonce) {
            super.onDownloadRequested(nonce);
            final Context context = getContext();
            if (context != null) {
                mMediaDownloadEvent = MediaDownloadEvent.create(context, getMedia(), nonce);
            } else {
                mMediaDownloadEvent = null;
            }
        }

        @Override
        public void onDownloadStart(long total, long nonce) {
            super.onDownloadStart(total, nonce);
            if (mMediaDownloadEvent != null && mMediaDownloadEvent.getNonce() == nonce) {
                mMediaDownloadEvent.setOpenedTime(System.currentTimeMillis());
                mMediaDownloadEvent.setSize(total);
            }
        }

        @Override
        public void onDownloadFinished(long nonce) {
            super.onDownloadFinished(nonce);
            if (mMediaDownloadEvent != null && mMediaDownloadEvent.getNonce() == nonce) {
                mMediaDownloadEvent.markEnd();
                HotMobiLogger.getInstance(getContext()).log(getAccountKey(), mMediaDownloadEvent);
                mMediaDownloadEvent = null;
            }
        }

        @Nullable
        private ParcelableMedia getMedia() {
            return getArguments().getParcelable(EXTRA_MEDIA);
        }

        private UserKey getAccountKey() {
            return getArguments().getParcelable(EXTRA_ACCOUNT_KEY);
        }

        private static class VideoPlayProgressRunnable implements Runnable {

            private final Handler mHandler;
            private final ProgressBar mProgressBar;
            private final TextView mDurationLabel, mPositionLabel;
            private final MediaController.MediaPlayerControl mMediaPlayerControl;

            VideoPlayProgressRunnable(Handler handler, ProgressBar progressBar, TextView durationLabel,
                    TextView positionLabel, MediaController.MediaPlayerControl mediaPlayerControl) {
                mHandler = handler;
                mProgressBar = progressBar;
                mDurationLabel = durationLabel;
                mPositionLabel = positionLabel;
                mMediaPlayerControl = mediaPlayerControl;
                mProgressBar.setMax(1000);
            }

            @Override
            public void run() {
                final int duration = mMediaPlayerControl.getDuration();
                final int position = mMediaPlayerControl.getCurrentPosition();
                if (duration <= 0 || position < 0)
                    return;
                mProgressBar.setProgress(Math.round(1000 * position / (float) duration));
                final long durationSecs = TimeUnit.SECONDS.convert(duration, TimeUnit.MILLISECONDS),
                        positionSecs = TimeUnit.SECONDS.convert(position, TimeUnit.MILLISECONDS);
                mDurationLabel
                        .setText(String.format(Locale.ROOT, "%02d:%02d", durationSecs / 60, durationSecs % 60));
                mPositionLabel
                        .setText(String.format(Locale.ROOT, "%02d:%02d", positionSecs / 60, positionSecs % 60));
                mHandler.postDelayed(this, 16);
            }
        }
    }

    public static class ExternalBrowserPageFragment extends MediaViewerFragment {
        private WebView mWebView;
        private AspectLockedFrameLayout mWebViewContainer;

        @Override
        protected View onCreateMediaView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
            return inflater.inflate(R.layout.layout_media_viewer_browser_fragment, parent, false);
        }

        @SuppressLint("SetJavaScriptEnabled")
        @Override
        public void onActivityCreated(@Nullable Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
            final WebSettings webSettings = mWebView.getSettings();
            webSettings.setJavaScriptEnabled(true);
            webSettings.setLoadsImagesAutomatically(true);
            final ParcelableMedia media = getArguments().getParcelable(EXTRA_MEDIA);
            if (media == null)
                throw new NullPointerException();
            mWebView.loadUrl(TextUtils.isEmpty(media.media_url) ? media.url : media.media_url);
            mWebViewContainer.setAspectRatioSource(new AspectLockedFrameLayout.AspectRatioSource() {
                @Override
                public int getWidth() {
                    return media.width;
                }

                @Override
                public int getHeight() {
                    return media.height;
                }
            });
        }

        @Override
        public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);
            mWebViewContainer = ((AspectLockedFrameLayout) view.findViewById(R.id.webview_container));
            mWebView = (WebView) view.findViewById(R.id.webview);
        }

        @Override
        public void onResume() {
            super.onResume();
            mWebView.onResume();
        }

        @Override
        public void onPause() {
            mWebView.onPause();
            super.onPause();
        }

        @Override
        public void onDestroy() {
            mWebView.destroy();
            super.onDestroy();
        }

        @Override
        protected void recycleMedia() {

        }

        @Override
        public void setUserVisibleHint(boolean isVisibleToUser) {
            super.setUserVisibleHint(isVisibleToUser);
            if (isVisibleToUser) {
                final FragmentActivity activity = getActivity();
                if (activity != null) {
                    activity.supportInvalidateOptionsMenu();
                }
            }
        }
    }
}