com.vuze.android.remote.fragment.FilesFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.vuze.android.remote.fragment.FilesFragment.java

Source

/**
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 * <p/>
 * 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */

package com.vuze.android.remote.fragment;

import java.io.File;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.support.v4.app.FragmentActivity;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.view.ActionMode;
import android.support.v7.view.ActionMode.Callback;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.text.Html;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.*;
import android.webkit.MimeTypeMap;
import android.widget.*;

import com.vuze.android.FlexibleRecyclerSelectionListener;
import com.vuze.android.remote.*;
import com.vuze.android.remote.SessionInfo.RpcExecuter;
import com.vuze.android.remote.activity.ImageViewer;
import com.vuze.android.remote.activity.VideoViewer;
import com.vuze.android.remote.adapter.*;
import com.vuze.android.remote.rpc.TorrentListReceivedListener;
import com.vuze.android.remote.rpc.TransmissionRPC;
import com.vuze.android.widget.PreCachingLayoutManager;
import com.vuze.android.widget.SwipeRefreshLayoutExtra;
import com.vuze.util.MapUtils;

/**
 * Shows the list of files with a torrent
 *
 * NOTE: There's duplicate code in OpenOptionsFileFragment.  Untill common code is merged,
 * changes here should be done there.
 *
 * TODO: Move progressbar logic out so all {@link TorrentDetailPage} can use it
 */
public class FilesFragment extends TorrentDetailPage implements ActionModeBeingReplacedListener, View.OnKeyListener,
        SwipeRefreshLayoutExtra.OnExtraViewVisibilityChangeListener {
    protected static final String TAG = "FilesFragment";

    /**
     * Launching an Intent without a Mime will result in a different list
     * of apps then one including the Mime type.  Sometimes one is better than
     * the other, especially with URLs :(
     *
     * Pros for setting MIME:
     * - In theory should provide more apps
     *
     * Cons for setting MIME:
     * - the Web browser will not show as an app, but rather a
     * html viewer app, if you are lucky
     * - A lot of apps that accept MIME types can't handle URLs and fail
     */
    protected static final boolean tryLaunchWithMimeFirst = false;

    private RecyclerView listview;

    private FilesTreeAdapter adapter;

    private Callback mActionModeCallback;

    protected ActionMode mActionMode;

    private final Object mLock = new Object();

    private int numProgresses = 0;

    private ActionModeBeingReplacedListener mCallback;

    private ProgressBar progressBar;

    private boolean showProgressBarOnAttach = false;

    private long lastUpdated = 0;

    private boolean refreshing;

    private View viewAreaToggleEditMode;

    private TextView tvScrollTitle;

    private CompoundButton btnEditMode;

    private Toolbar tb;

    private Handler pullRefreshHandler;

    public FilesFragment() {
        super();
    }

    @Override
    public void onAttach(Context context) {
        if (AndroidUtils.DEBUG) {
            Log.d(TAG, "onAttach " + this + " to " + context);
        }
        super.onAttach(context);

        if (showProgressBarOnAttach) {
            showProgressBar();
        }

        if (context instanceof ActionModeBeingReplacedListener) {
            mCallback = (ActionModeBeingReplacedListener) context;
        }
    }

    /* (non-Javadoc)
     * @see android.support.v4.app.Fragment#onActivityCreated(android.os.Bundle)
     */
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        tb = (Toolbar) getActivity().findViewById(R.id.toolbar_bottom);
    }

    private void showProgressBar() {
        synchronized (mLock) {
            numProgresses++;
            if (AndroidUtils.DEBUG) {
                Log.d(TAG, "showProgress " + numProgresses);
            }
        }
        FragmentActivity activity = getActivity();
        if (activity == null || progressBar == null) {
            showProgressBarOnAttach = true;
            return;
        }

        progressBar.postDelayed(new Runnable() {
            @Override
            public void run() {
                FragmentActivity activity = getActivity();
                if (activity == null || numProgresses <= 0) {
                    return;
                }
                progressBar.setVisibility(View.VISIBLE);
            }
        }, 600);
    }

    private void hideProgressBar() {
        synchronized (mLock) {
            numProgresses--;
            if (AndroidUtils.DEBUG) {
                Log.d(TAG, "hideProgress " + numProgresses);
            }
            if (numProgresses <= 0) {
                numProgresses = 0;
            } else {
                return;
            }
        }
        FragmentActivity activity = getActivity();
        if (activity == null || progressBar == null) {
            showProgressBarOnAttach = false;
            return;
        }
        AndroidUtils.runOnUIThread(this, new Runnable() {
            @Override
            public void run() {
                progressBar.setVisibility(View.GONE);
            }
        });
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        if (AndroidUtils.DEBUG) {
            Log.d(TAG, "onCreate " + this);
        }
        super.onCreate(savedInstanceState);

        setupActionModeCallback();
    }

    public View onCreateView(android.view.LayoutInflater inflater, android.view.ViewGroup container,
            Bundle savedInstanceState) {

        if (AndroidUtils.DEBUG) {
            Log.d(TAG, "onCreateview " + this);
        }

        View view = inflater.inflate(R.layout.frag_torrent_files, container, false);

        FragmentActivity activity = getActivity();

        progressBar = (ProgressBar) activity.findViewById(R.id.details_progress_bar);

        viewAreaToggleEditMode = view.findViewById(R.id.files_area_toggleditmode);
        tvScrollTitle = (TextView) view.findViewById(R.id.files_scrolltitle);

        btnEditMode = (CompoundButton) view.findViewById(R.id.files_editmode);
        if (btnEditMode != null) {
            btnEditMode.setOnClickListener(new View.OnClickListener() {

                @Override
                public void onClick(View v) {
                    if (adapter == null) {
                        return;
                    }
                    adapter.setInEditMode(btnEditMode.isChecked());
                }
            });
        }

        final SwipeRefreshLayoutExtra swipeRefresh = (SwipeRefreshLayoutExtra) view
                .findViewById(R.id.swipe_container);
        if (swipeRefresh != null) {
            swipeRefresh.setExtraLayout(R.layout.swipe_layout_extra);
            swipeRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
                @Override
                public void onRefresh() {
                    if (sessionInfo == null) {
                        return;
                    }
                    showProgressBar();
                    sessionInfo.executeRpc(new RpcExecuter() {
                        @Override
                        public void executeRpc(TransmissionRPC rpc) {
                            rpc.getTorrentFileInfo(TAG, torrentID, null, new TorrentListReceivedListener() {

                                @Override
                                public void rpcTorrentListReceived(String callID, List<?> addedTorrentMaps,
                                        List<?> removedTorrentIDs) {
                                    AndroidUtils.runOnUIThread(FilesFragment.this, new Runnable() {
                                        @Override
                                        public void run() {
                                            swipeRefresh.setRefreshing(false);
                                        }
                                    });
                                }
                            });
                        }
                    });

                }
            });
            swipeRefresh.setOnExtraViewVisibilityChange(this);
        }

        listview = (RecyclerView) view.findViewById(R.id.files_list);
        listview.setLayoutManager(new PreCachingLayoutManager(getContext()));
        listview.setAdapter(adapter);

        listview.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                {
                    if (event.getAction() != KeyEvent.ACTION_DOWN) {
                        return false;
                    }
                    switch (keyCode) {
                    case KeyEvent.KEYCODE_DPAD_RIGHT: {
                        // expand
                        int i = adapter.getSelectedPosition();
                        FilesAdapterDisplayObject item = adapter.getItem(i);
                        if (item instanceof FilesAdapterDisplayFolder) {
                            if (!((FilesAdapterDisplayFolder) item).expand) {
                                ((FilesAdapterDisplayFolder) item).expand = true;
                                adapter.getFilter().filter("");
                                return true;
                            }
                        }
                        break;
                    }

                    case KeyEvent.KEYCODE_DPAD_LEFT: {
                        // collapse
                        int i = adapter.getSelectedPosition();
                        FilesAdapterDisplayObject item = adapter.getItem(i);
                        if (item instanceof FilesAdapterDisplayFolder) {
                            if (((FilesAdapterDisplayFolder) item).expand) {
                                ((FilesAdapterDisplayFolder) item).expand = false;
                                adapter.getFilter().filter("");
                                return true;
                            }
                        }
                        break;
                    }

                    case KeyEvent.KEYCODE_MEDIA_PLAY:
                    case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: {
                        launchOrStreamFile();
                        return true;
                    }
                    }

                    return false;
                }
            }
        });

        listview.setOnScrollListener(new RecyclerView.OnScrollListener() {
            int firstVisibleItem = 0;

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                LinearLayoutManager lm = (LinearLayoutManager) listview.getLayoutManager();
                int firstVisibleItem = lm.findFirstCompletelyVisibleItemPosition();
                if (firstVisibleItem != this.firstVisibleItem) {
                    this.firstVisibleItem = firstVisibleItem;
                    FilesAdapterDisplayObject itemAtPosition = adapter.getItem(firstVisibleItem);
                    //               Log.d(TAG, "itemAt" + firstVisibleItem + " is " + itemAtPosition);
                    //               Log.d(TAG, "tvScrollTitle=" + tvScrollTitle);
                    //               Log.d(TAG, "viewAreaToggleEditMode=" + viewAreaToggleEditMode);

                    if (itemAtPosition == null) {
                        return;
                    }
                    if (itemAtPosition.parent != null) {
                        if (viewAreaToggleEditMode != null) {
                            viewAreaToggleEditMode.setVisibility(View.GONE);
                        }
                        if (tvScrollTitle != null) {
                            tvScrollTitle.setVisibility(View.VISIBLE);
                            tvScrollTitle.setText(itemAtPosition.parent.folder);
                        }
                    } else {
                        if (viewAreaToggleEditMode != null) {
                            viewAreaToggleEditMode.setVisibility(View.VISIBLE);
                        }
                        if (tvScrollTitle != null) {
                            if (viewAreaToggleEditMode != null) {
                                tvScrollTitle.setVisibility(View.INVISIBLE);
                            }
                            tvScrollTitle.setText("");
                        }
                    }
                }
            }
        });

        FlexibleRecyclerSelectionListener rs = new FlexibleRecyclerSelectionListener<FilesTreeAdapter, FilesAdapterDisplayObject>() {
            @Override
            public void onItemSelected(FilesTreeAdapter adapter, final int position, boolean isChecked) {
            }

            @Override
            public void onItemClick(FilesTreeAdapter adapter, int position) {
                if (AndroidUtils.usesNavigationControl()) {
                    FilesAdapterDisplayObject oItem = adapter.getItem(position);
                    if (adapter.isInEditMode()) {
                        adapter.flipWant(oItem);
                        return;
                    }
                    if (oItem instanceof FilesAdapterDisplayFolder) {
                        FilesAdapterDisplayFolder oFolder = (FilesAdapterDisplayFolder) oItem;
                        oFolder.expand = !oFolder.expand;
                        adapter.getFilter().filter("");
                    } else {
                        showFileContextMenu();
                    }
                }
            }

            @Override
            public boolean onItemLongClick(FilesTreeAdapter adapter, int position) {
                if (AndroidUtils.usesNavigationControl()) {
                    if (showFileContextMenu()) {
                        return true;
                    }
                }
                return false;
            }

            @Override
            public void onItemCheckedChanged(FilesTreeAdapter adapter, FilesAdapterDisplayObject item,
                    boolean isChecked) {

                if (adapter.getCheckedItemCount() == 0) {
                    finishActionMode();
                } else {
                    // Update the subtitle with file name
                    showContextualActions();
                }

                AndroidUtils.invalidateOptionsMenuHC(getActivity(), mActionMode);
            }
        };

        adapter = new FilesTreeAdapter(this.getActivity(), rs);
        adapter.setSessionInfo(sessionInfo);
        adapter.setMultiCheckModeAllowed(false);
        adapter.setCheckOnSelectedAfterMS(100);
        listview.setAdapter(adapter);

        return view;
    }

    /* (non-Javadoc)
      * @see com.vuze.android.remote.fragment
      * .TorrentDetailPage#updateTorrentID(long, boolean, boolean, boolean)
      */
    @Override
    public void updateTorrentID(final long torrentID, boolean isTorrent, boolean wasTorrent,
            boolean torrentIdChanged) {
        if (torrentIdChanged) {
            adapter.removeAllItems();
        }

        if (!wasTorrent && isTorrent) {
            if (AndroidUtils.DEBUG) {
                Log.d(TAG, "setTorrentID: add listener");
            }
            sessionInfo.addTorrentListReceivedListener(this, false);
        } else if (wasTorrent && !isTorrent) {
            if (AndroidUtils.DEBUG) {
                Log.d(TAG, "setTorrentID: remove listener");
            }
            sessionInfo.removeTorrentListReceivedListener(this);
        }

        //System.out.println("torrent is " + torrent);
        adapter.setSessionInfo(sessionInfo);
        if (isTorrent) {
            Map<?, ?> torrent = sessionInfo.getTorrent(torrentID);
            if (torrent == null) {
                Log.e(TAG, "setTorrentID: No torrent #" + torrentID);
            } else {

                if (torrent.containsKey(TransmissionVars.FIELD_TORRENT_FILES)) {
                    // already has files.. we are good to go, although might be a bit
                    // outdated
                    adapter.setTorrentID(torrentID);
                } else {
                    triggerRefresh();
                }
            }
        } else {
            synchronized (mLock) {
                numProgresses = 1;
                hideProgressBar();
            }
        }

        if (torrentIdChanged) {
            adapter.clearChecked();
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        outState.putLong("torrentID", torrentID);
    }

    private void setupActionModeCallback() {
        mActionModeCallback = new Callback() {

            // Called when the action mode is created; startActionMode() was called
            @Override
            public boolean onCreateActionMode(ActionMode mode, Menu menu) {
                if (AndroidUtils.DEBUG_MENU) {
                    Log.d(TAG, "onCreateActionMode");
                }

                if (adapter.getSelectedPosition() < 0) {
                    return false;
                }

                if (mode == null) {
                    getActivity().getMenuInflater().inflate(R.menu.menu_context_torrent_files, menu);
                    onPrepareOptionsMenu(menu);
                    return true;
                }

                mActionMode = new ActionModeWrapperV7(mode, tb, getActivity());

                // Inflate a menu resource providing context menu items
                ActionBarToolbarSplitter.buildActionBar(getActivity(), this, R.menu.menu_context_torrent_files,
                        menu, tb);

                return true;
            }

            // Called each time the action mode is shown. Always called after
            // onCreateActionMode, but
            // may be called multiple times if the mode is invalidated.
            @Override
            public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
                if (AndroidUtils.DEBUG_MENU) {
                    Log.d(TAG, "onPrepareActionMode");
                }

                if (mode != null && tb != null) {
                    menu = tb.getMenu();
                }
                return prepareContextMenu(menu);
            }

            // Called when the user selects a contextual menu item
            @Override
            public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
                return handleMenu(item.getItemId());
            }

            // Called when the user exits the action mode
            @Override
            public void onDestroyActionMode(ActionMode mode) {
                destroyActionMode();
            }
        };
    }

    private void destroyActionMode() {
        if (AndroidUtils.DEBUG_MENU) {
            Log.d(TAG, "destroyActionMode");
        }
        if (mActionMode == null) {
            return;
        }
        mActionMode = null;

        adapter.clearChecked();

        // delay so actionmode finishes up
        listview.post(new Runnable() {

            @Override
            public void run() {
                if (mCallback != null) {
                    mCallback.actionModeBeingReplacedDone();
                }
            }
        });
    }

    protected boolean prepareContextMenu(Menu menu) {
        if (sessionInfo == null || torrentID < 0) {
            return false;
        }

        boolean isComplete = false;
        Map<?, ?> mapFile = getFocusedFile();
        boolean enable = mapFile != null && mapFile.size() > 0;
        if (mapFile != null) {
            long bytesCompleted = MapUtils.getMapLong(mapFile, "bytesCompleted", 0);
            long length = MapUtils.getMapLong(mapFile, "length", -1);
            //System.out.println("mapFIle=" + mapFile);
            isComplete = bytesCompleted == length;
        }

        boolean isLocalHost = sessionInfo != null && sessionInfo.getRemoteProfile().isLocalHost();
        boolean isOnlineOrLocal = VuzeRemoteApp.getNetworkState().isOnline() || isLocalHost;

        MenuItem menuLaunch = menu.findItem(R.id.action_sel_launch);
        if (menuLaunch != null) {
            if (enable && sessionInfo.getRemoteProfile().isLocalHost()) {
                boolean canLaunch = isComplete;
                canLaunch &= isOnlineOrLocal;
                menuLaunch.setEnabled(canLaunch);
                menuLaunch.setVisible(true);
            } else {
                menuLaunch.setVisible(false);
            }
        }

        MenuItem menuLaunchStream = menu.findItem(R.id.action_sel_launch_stream);
        if (menuLaunchStream != null) {
            boolean canStream = enable && isComplete
                    && mapFile.containsKey(TransmissionVars.FIELD_FILES_CONTENT_URL);
            canStream &= isOnlineOrLocal;
            menuLaunchStream.setEnabled(canStream);
        }

        MenuItem menuSave = menu.findItem(R.id.action_sel_save);
        if (menuSave != null) {
            boolean visible = !isLocalHost;
            menuSave.setVisible(visible);
            if (visible) {
                boolean canSave = enable && isOnlineOrLocal && isComplete
                        && mapFile.containsKey(TransmissionVars.FIELD_FILES_CONTENT_URL);
                menuSave.setEnabled(canSave);
            }
        }

        int priority = MapUtils.getMapInt(mapFile, TransmissionVars.FIELD_TORRENT_FILES_PRIORITY,
                TransmissionVars.TR_PRI_NORMAL);
        MenuItem menuPriorityUp = menu.findItem(R.id.action_sel_priority_up);
        if (menuPriorityUp != null) {
            menuPriorityUp.setEnabled(
                    enable && isOnlineOrLocal && !isComplete && priority < TransmissionVars.TR_PRI_HIGH);
        }
        MenuItem menuPriorityDown = menu.findItem(R.id.action_sel_priority_down);
        if (menuPriorityDown != null) {
            menuPriorityDown
                    .setEnabled(enable && isOnlineOrLocal && !isComplete && priority > TransmissionVars.TR_PRI_LOW);
        }

        boolean wanted = MapUtils.getMapBoolean(mapFile, "wanted", true);
        MenuItem menuUnwant = menu.findItem(R.id.action_sel_unwanted);
        if (menuUnwant != null) {
            menuUnwant.setVisible(wanted);
            menuUnwant.setEnabled(enable && isOnlineOrLocal);
        }
        MenuItem menuWant = menu.findItem(R.id.action_sel_wanted);
        if (menuWant != null) {
            menuWant.setVisible(!wanted);
            menuWant.setEnabled(enable && isOnlineOrLocal);
        }

        AndroidUtils.fixupMenuAlpha(menu);
        return true;
    }

    protected boolean handleMenu(int itemId) {
        if (sessionInfo == null || torrentID < 0) {
            return false;
        }
        if (itemId == R.id.action_sel_launch) {
            Map<?, ?> selectedFile = getFocusedFile();
            return selectedFile != null && launchLocalFile(selectedFile);
        } else if (itemId == R.id.action_sel_launch_stream) {
            Map<?, ?> selectedFile = getFocusedFile();
            return selectedFile != null && streamFile(selectedFile);
        } else if (itemId == R.id.action_sel_save) {
            Map<?, ?> selectedFile = getFocusedFile();
            return saveFile(selectedFile);
        } else if (itemId == R.id.action_sel_wanted) {
            showProgressBar();
            sessionInfo.executeRpc(new RpcExecuter() {
                @Override
                public void executeRpc(TransmissionRPC rpc) {
                    rpc.setWantState(TAG, torrentID, new int[] { getFocusedFileIndex() }, true, null);
                }
            });
            return true;
        } else if (itemId == R.id.action_sel_unwanted) {
            // TODO: Delete Prompt
            showProgressBar();
            sessionInfo.executeRpc(new RpcExecuter() {

                @Override
                public void executeRpc(TransmissionRPC rpc) {
                    rpc.setWantState(TAG, torrentID, new int[] { getFocusedFileIndex() }, false, null);
                }
            });
            return true;
        } else if (itemId == R.id.action_sel_priority_up) {

            Map<?, ?> selectedFile = getFocusedFile();

            int priority = MapUtils.getMapInt(selectedFile, TransmissionVars.FIELD_TORRENT_FILES_PRIORITY,
                    TransmissionVars.TR_PRI_NORMAL);
            if (priority >= TransmissionVars.TR_PRI_HIGH) {
                return true;
            } else {
                priority += 1;
            }

            showProgressBar();
            final int fpriority = priority;
            sessionInfo.executeRpc(new RpcExecuter() {
                @Override
                public void executeRpc(TransmissionRPC rpc) {
                    rpc.setFilePriority(TAG, torrentID, new int[] { getFocusedFileIndex() }, fpriority, null);
                }
            });
            return true;
        } else if (itemId == R.id.action_sel_priority_down) {
            Map<?, ?> selectedFile = getFocusedFile();
            int priority = MapUtils.getMapInt(selectedFile, TransmissionVars.FIELD_TORRENT_FILES_PRIORITY,
                    TransmissionVars.TR_PRI_NORMAL);
            if (priority <= TransmissionVars.TR_PRI_LOW) {
                return true;
            } else {
                priority -= 1;
            }
            showProgressBar();
            final int fpriority = priority;
            sessionInfo.executeRpc(new RpcExecuter() {

                @Override
                public void executeRpc(TransmissionRPC rpc) {
                    rpc.setFilePriority(TAG, torrentID, new int[] { getFocusedFileIndex() }, fpriority, null);
                }

            });
            return true;
        }
        return false;
    }

    protected boolean saveFile(Map<?, ?> selectedFile) {
        if (selectedFile == null) {
            return false;
        }
        if (sessionInfo == null) {
            return false;
        }
        if (sessionInfo.getRemoteProfile().isLocalHost()) {
            return false;
        }
        final String contentURL = getContentURL(selectedFile);
        if (contentURL == null || contentURL.length() == 0) {
            return false;
        }

        final File directory = AndroidUtils.getDownloadDir();
        final File outFile = new File(directory, MapUtils.getMapString(selectedFile, "name", "foo.txt"));

        if (VuzeRemoteApp.getNetworkState().isOnlineMobile()) {
            Resources resources = getActivity().getResources();
            String message = resources.getString(R.string.on_mobile,
                    resources.getString(R.string.save_content, TextUtils.htmlEncode(outFile.getName())));
            Builder builder = new AlertDialog.Builder(getActivity()).setMessage(Html.fromHtml(message))
                    .setPositiveButton(R.string.yes, new OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            saveFile(contentURL, outFile);
                        }
                    }).setNegativeButton(R.string.no, null);
            builder.show();
            return true;
        }

        saveFile(contentURL, outFile);

        return true;
    }

    private String getContentURL(Map<?, ?> selectedFile) {
        String contentURL = MapUtils.getMapString(selectedFile, "contentURL", null);
        if (contentURL == null || contentURL.length() == 0) {
            return contentURL;
        }
        if (contentURL.charAt(0) == ':' || contentURL.charAt(0) == '/') {
            contentURL = sessionInfo.getBaseURL() + contentURL;
        }
        if (contentURL.contains("/localhost:")) {
            return contentURL.replaceAll("/localhost:",
                    "/" + VuzeRemoteApp.getNetworkState().getActiveIpAddress() + ":");
        }

        return contentURL;
    }

    protected void saveFile(final String contentURL, final File outFile) {
        requestPermissions(new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, new Runnable() {
            @Override
            public void run() {
                reallySaveFile(contentURL, outFile);
            }
        }, new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getActivity(), R.string.content_saved_failed_perms_denied, Toast.LENGTH_LONG).show();
            }
        });
    }

    private void reallySaveFile(final String contentURL, final File outFile) {
        showProgressBar();
        new Thread(new Runnable() {
            String failText = null;

            @Override
            public void run() {
                try {
                    AndroidUtils.copyUrlToFile(contentURL, outFile);
                } catch (Exception e) {
                    VuzeEasyTracker.getInstance().logError(e);
                    failText = e.getMessage();
                }
                FragmentActivity activity = getActivity();
                if (activity == null) {
                    return;
                }
                activity.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        hideProgressBar();
                        Activity activity = getActivity();
                        Context context = activity == null ? VuzeRemoteApp.getContext() : activity;
                        String s;
                        if (failText == null) {
                            s = context.getResources().getString(R.string.content_saved,
                                    TextUtils.htmlEncode(outFile.getName()),
                                    TextUtils.htmlEncode(outFile.getParent()));
                        } else {
                            s = context.getResources().getString(R.string.content_saved_failed,
                                    TextUtils.htmlEncode(outFile.getName()),
                                    TextUtils.htmlEncode(outFile.getParent()), TextUtils.htmlEncode(failText));
                        }
                        Toast.makeText(context, Html.fromHtml(s), Toast.LENGTH_SHORT).show();
                    }
                });
            }
        }).start();
    }

    protected boolean streamFile(final Map<?, ?> selectedFile) {

        if (VuzeRemoteApp.getNetworkState().isOnlineMobile()) {
            String name = MapUtils.getMapString(selectedFile, "name", null);

            Resources resources = getActivity().getResources();
            String message = resources.getString(R.string.on_mobile,
                    resources.getString(R.string.stream_content, TextUtils.htmlEncode(name)));
            Builder builder = new AlertDialog.Builder(getActivity()).setMessage(Html.fromHtml(message))
                    .setPositiveButton(R.string.yes, new OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            reallyStreamFile(selectedFile);
                        }
                    }).setNegativeButton(R.string.no, null);
            builder.show();
        } else {
            reallyStreamFile(selectedFile);
        }
        return true;
    }

    protected boolean launchLocalFile(Map<?, ?> selectedFile) {

        String fullPath = MapUtils.getMapString(selectedFile, "fullPath", null);
        if (fullPath != null && fullPath.length() > 0) {
            File file = new File(fullPath);
            if (file.exists()) {
                Uri uri = Uri.fromFile(file);
                Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                try {
                    startActivity(intent);
                } catch (android.content.ActivityNotFoundException ignore) {

                }
                if (AndroidUtils.DEBUG) {
                    Log.d(TAG, "Started " + uri);
                }
                return true;
            } else {
                if (AndroidUtils.DEBUG) {
                    Log.d(TAG, "Launch: File Not Found: " + fullPath);
                }
            }
        }

        return false;
    }

    @SuppressWarnings("unused")
    protected boolean reallyStreamFile(Map<?, ?> selectedFile) {
        final String contentURL = getContentURL(selectedFile);
        if (contentURL != null && contentURL.length() > 0) {
            Uri uri = Uri.parse(contentURL);

            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
            String name = MapUtils.getMapString(selectedFile, "name", "video");
            intent.putExtra("title", name);

            String extension = MimeTypeMap.getFileExtensionFromUrl(contentURL).toLowerCase(Locale.US);
            String mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
            if (mimetype != null && tryLaunchWithMimeFirst) {
                intent.setType(mimetype);
            }
            Class<?> fallBackIntentClass = VideoViewer.class;
            if (mimetype != null && mimetype.startsWith("image")) {
                fallBackIntentClass = ImageViewer.class;
            }

            final PackageManager packageManager = getActivity().getPackageManager();
            List<ResolveInfo> list = packageManager.queryIntentActivities(intent,
                    PackageManager.MATCH_DEFAULT_ONLY);
            if (AndroidUtils.DEBUG) {
                Log.d(TAG, "num intents " + list.size());
                for (ResolveInfo info : list) {
                    ComponentInfo componentInfo = AndroidUtils.getComponentInfo(info);
                    Log.d(TAG, info.toString() + "/"
                            + (componentInfo == null ? "null" : (componentInfo.name + "/" + componentInfo)));
                }
            }
            if (list.size() == 0) {
                // Intent will launch, but show message to the user:
                // "Opening web browser links is not supported"
                intent.setClass(getActivity(), fallBackIntentClass);
            }
            if (list.size() == 1) {
                ResolveInfo info = list.get(0);
                ComponentInfo componentInfo = AndroidUtils.getComponentInfo(info);
                if (componentInfo != null && componentInfo.name != null) {
                    if ("com.amazon.unifiedshare.actionchooser.BuellerShareActivity".equals(componentInfo.name)
                            || componentInfo.name.startsWith("com.google.android.tv.frameworkpackagestubs.Stubs")) {
                        intent.setClass(getActivity(), fallBackIntentClass);
                    }
                }
            }

            try {
                startActivity(intent);
                if (AndroidUtils.DEBUG) {
                    Log.d(TAG, "Started " + uri + " MIME: " + intent.getType());
                }
            } catch (java.lang.SecurityException es) {
                if (AndroidUtils.DEBUG) {
                    Log.d(TAG, "ERROR launching. " + es.toString());
                }

                if (mimetype != null) {
                    try {
                        Intent intent2 = new Intent(Intent.ACTION_VIEW, uri);
                        intent2.putExtra("title", name);
                        if (!tryLaunchWithMimeFirst) {
                            intent2.setType(mimetype);
                        }

                        list = packageManager.queryIntentActivities(intent2, PackageManager.MATCH_DEFAULT_ONLY);
                        if (AndroidUtils.DEBUG) {
                            Log.d(TAG, "num intents " + list.size());
                            for (ResolveInfo info : list) {
                                ComponentInfo componentInfo = AndroidUtils.getComponentInfo(info);
                                Log.d(TAG, info.toString() + "/" + (componentInfo == null ? "null"
                                        : (componentInfo.name + "/" + componentInfo)));
                            }
                        }

                        startActivity(intent2);
                        if (AndroidUtils.DEBUG) {
                            Log.d(TAG,
                                    "Started with" + (intent2.getType() == null ? " no" : " ") + " mime: " + uri);
                        }
                        return true;
                    } catch (Throwable ex2) {
                        if (AndroidUtils.DEBUG) {
                            Log.d(TAG, "no intent for view. " + ex2.toString());
                        }
                    }
                }

                Toast.makeText(getActivity().getApplicationContext(),
                        getActivity().getResources().getString(R.string.intent_security_fail), Toast.LENGTH_LONG)
                        .show();
            } catch (android.content.ActivityNotFoundException ex) {
                if (AndroidUtils.DEBUG) {
                    Log.d(TAG, "no intent for view. " + ex.toString());
                }

                if (mimetype != null) {
                    try {
                        Intent intent2 = new Intent(Intent.ACTION_VIEW, uri);
                        intent2.putExtra("title", name);
                        if (!tryLaunchWithMimeFirst) {
                            intent2.setType(mimetype);
                        }
                        startActivity(intent2);
                        if (AndroidUtils.DEBUG) {
                            Log.d(TAG, "Started (no mime set) " + uri);
                        }
                        return true;
                    } catch (android.content.ActivityNotFoundException ex2) {
                        if (AndroidUtils.DEBUG) {
                            Log.d(TAG, "no intent for view. " + ex2.toString());
                        }
                    }
                }

                Toast.makeText(getActivity().getApplicationContext(),
                        getActivity().getResources().getString(R.string.no_intent), Toast.LENGTH_SHORT).show();
            }
            return true;
        }

        return true;
    }

    private int getFocusedFileIndex() {
        Map<?, ?> torrent = sessionInfo.getTorrent(torrentID);
        if (torrent == null) {
            return -1;
        }
        List<?> listFiles = MapUtils.getMapList(torrent, TransmissionVars.FIELD_TORRENT_FILES, null);
        int selectedPosition = adapter.getSelectedPosition();
        long id = adapter.getItemId(selectedPosition);
        if (listFiles == null || id < 0 || id >= listFiles.size()) {
            return -1;
        }
        if (AndroidUtils.DEBUG) {
            Log.d(TAG, "FocusedFile #" + id);
        }
        Object object = listFiles.get((int) id);
        if (object instanceof Map<?, ?>) {
            return (int) id;
        }
        return -1;
    }

    protected Map<?, ?> getFocusedFile() {
        FilesAdapterDisplayObject selectedItem = adapter.getSelectedItem();
        if (selectedItem == null) {
            return null;
        }
        if (selectedItem instanceof FilesAdapterDisplayFolder) {
            return null;
        }

        return selectedItem.getMap(sessionInfo, torrentID);
    }

    protected boolean showContextualActions() {
        if (AndroidUtils.isTV()) {
            // TV doesn't get action bar changes, because it's impossible to get to
            // with remote control when you are on row 4000
            return false;
        }
        if (mActionMode != null) {
            if (AndroidUtils.DEBUG_MENU) {
                Log.d(TAG, "showContextualActions: invalidate existing");
            }
            Map<?, ?> selectedFile = getFocusedFile();
            String name = MapUtils.getMapString(selectedFile, "name", null);
            mActionMode.setSubtitle(name);

            mActionMode.invalidate();
            return false;
        }

        if (mCallback != null) {
            mCallback.setActionModeBeingReplaced(null, true);
        }
        AppCompatActivity activity = (AppCompatActivity) getActivity();
        if (activity == null) {
            return false;
        }
        if (AndroidUtils.DEBUG_MENU) {
            Log.d(TAG,
                    "showContextualActions: startAB. mActionMode = " + mActionMode + "; isShowing="
                            + (activity.getSupportActionBar() == null ? "null"
                                    : activity.getSupportActionBar().isShowing()));
        }
        // Start the CAB using the ActionMode.Callback defined above
        ActionMode am = activity.startSupportActionMode(mActionModeCallback);
        if (am == null) {
            Log.d(TAG, "showContextualActions: startSupportsActionMode returned null");
            return false;
        }
        mActionMode = new ActionModeWrapperV7(am, tb, getActivity());

        mActionMode.setTitle(R.string.context_file_title);
        Map<?, ?> selectedFile = getFocusedFile();
        String name = MapUtils.getMapString(selectedFile, "name", null);
        mActionMode.setSubtitle(name);
        if (mCallback != null) {
            mCallback.setActionModeBeingReplaced(mActionMode, false);
        }
        return true;
    }

    public void finishActionMode() {
        if (mActionMode != null) {
            mActionMode.finish();
            mActionMode = null;
        }
    }

    /* (non-Javadoc)
     * @see com.vuze.android.remote.rpc
     * .TorrentListReceivedListener#rpcTorrentListReceived(java.util.List)
     */
    @SuppressWarnings("rawtypes")
    @Override
    public void rpcTorrentListReceived(String callID, final List<?> addedTorrentMaps, List<?> removedTorrentIDs) {
        boolean found = false;
        for (Object item : addedTorrentMaps) {
            if (!(item instanceof Map)) {
                continue;
            }
            Map mapTorrent = (Map) item;
            Object key = mapTorrent.get("id");

            if (key instanceof Number) {
                found = ((Number) key).longValue() == torrentID;
                if (found) {
                    if (AndroidUtils.DEBUG) {
                        Log.d(TAG, "TorrentListReceived, contains torrent #" + torrentID);
                    }
                    break;
                }
            }
        }
        if (!found) {
            if (AndroidUtils.DEBUG) {
                Log.d(TAG, "TorrentListReceived, does not contain torrent #" + torrentID);
            }
            return;
        }
        // Not accurate when we are triggered because of addListener
        lastUpdated = System.currentTimeMillis();

        AndroidUtils.runOnUIThread(this, new AndroidUtils.RunnableWithActivity() {
            @Override
            public void run() {
                hideProgressBar();
                if (adapter != null) {
                    adapter.setTorrentID(torrentID);
                }
                AndroidUtils.invalidateOptionsMenuHC(activity, mActionMode);
            }
        });
    }

    @Override
    public void triggerRefresh() {
        if (sessionInfo == null || torrentID < 0) {
            return;
        }
        synchronized (mLock) {
            if (refreshing) {
                if (AndroidUtils.DEBUG) {
                    Log.d(TAG, "Skipping Refresh");
                }
                return;
            }
            refreshing = true;
        }

        showProgressBar();
        sessionInfo.executeRpc(new RpcExecuter() {
            @Override
            public void executeRpc(TransmissionRPC rpc) {
                rpc.getTorrentFileInfo(TAG, torrentID, null, new TorrentListReceivedListener() {

                    @Override
                    public void rpcTorrentListReceived(String callID, List<?> addedTorrentMaps,
                            List<?> removedTorrentIDs) {
                        synchronized (mLock) {
                            refreshing = false;
                        }
                    }
                });
            }
        });
    }

    @Override
    public void pageDeactivated() {
        finishActionMode();
        synchronized (mLock) {
            refreshing = false;
        }
        super.pageDeactivated();
    }

    public void launchOrStreamFile() {
        Map<?, ?> selectedFile = getFocusedFile();
        if (selectedFile == null) {
            return;
        }
        boolean isLocalHost = sessionInfo != null && sessionInfo.getRemoteProfile().isLocalHost();
        if (isLocalHost) {
            launchLocalFile(selectedFile);
        } else {
            streamFile(selectedFile);
        }
    }

    /* (non-Javadoc)
     * @see com.vuze.android.remote.fragment.TorrentDetailPage#getTAG()
     */
    @Override
    String getTAG() {
        return TAG;
    }

    /* (non-Javadoc)
     * @see com.vuze.android.remote.fragment
     * .ActionModeBeingReplacedListener#setActionModeBeingReplaced(android
     * .support.v7.view.ActionMode, boolean)
     */
    @Override
    public void setActionModeBeingReplaced(ActionMode actionMode, boolean actionModeBeingReplaced) {
    }

    /* (non-Javadoc)
     * @see com.vuze.android.remote.fragment
     * .ActionModeBeingReplacedListener#actionModeBeingReplacedDone()
     */
    @Override
    public void actionModeBeingReplacedDone() {
    }

    /* (non-Javadoc)
     * @see com.vuze.android.remote.fragment
     * .ActionModeBeingReplacedListener#rebuildActionMode()
     */
    @Override
    public void rebuildActionMode() {
    }

    /* (non-Javadoc)
     * @see com.vuze.android.remote.fragment
     * .ActionModeBeingReplacedListener#getActionMode()
     */
    @Override
    public ActionMode getActionMode() {
        return mActionMode;
    }

    @Override
    public Callback getActionModeCallback() {
        return mActionModeCallback;
    }

    @Override
    public boolean onKey(View v, int keyCode, KeyEvent event) {
        if (event.getAction() != KeyEvent.ACTION_DOWN) {
            return false;
        }
        switch (keyCode) {
        // NOTE:
        // KeyCharacterMap.deviceHasKey(KeyEvent.KEYCODE_MENU);
        case KeyEvent.KEYCODE_MENU:
        case KeyEvent.KEYCODE_BUTTON_X:
        case KeyEvent.KEYCODE_INFO: {
            if (tb == null) {
                if (showFileContextMenu()) {
                    return true;
                }
            }
            break;
        }
        }

        return false;
    }

    private boolean showFileContextMenu() {
        int selectedPosition = adapter.getSelectedPosition();
        if (selectedPosition < 0) {
            return false;
        }
        FilesAdapterDisplayObject item = adapter.getItem(selectedPosition);

        // TODO: Menu for folder actions
        if (!(item instanceof FilesAdapterDisplayFile)) {
            return false;
        }

        String s = getResources().getString(R.string.file_actions_for, item.name);
        return AndroidUtilsUI.popupContextMenu(getContext(), this, s);
    }

    @Override
    public void onExtraViewVisibilityChange(final View view, int visibility) {
        if (pullRefreshHandler != null) {
            pullRefreshHandler.removeCallbacksAndMessages(null);
            pullRefreshHandler = null;
        }
        if (visibility != View.VISIBLE) {
            return;
        }

        pullRefreshHandler = new Handler(Looper.getMainLooper());
        pullRefreshHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                FragmentActivity activity = getActivity();
                if (activity == null) {
                    return;
                }
                long sinceMS = System.currentTimeMillis() - lastUpdated;
                String since = DateUtils.getRelativeDateTimeString(activity, lastUpdated,
                        DateUtils.SECOND_IN_MILLIS, DateUtils.WEEK_IN_MILLIS, 0).toString();
                String s = activity.getResources().getString(R.string.last_updated, since);

                TextView tvSwipeText = (TextView) view.findViewById(R.id.swipe_text);
                tvSwipeText.setText(s);

                if (pullRefreshHandler != null) {
                    pullRefreshHandler.postDelayed(this,
                            sinceMS < DateUtils.MINUTE_IN_MILLIS ? DateUtils.SECOND_IN_MILLIS
                                    : sinceMS < DateUtils.HOUR_IN_MILLIS ? DateUtils.MINUTE_IN_MILLIS
                                            : DateUtils.HOUR_IN_MILLIS);
                }
            }
        }, 0);
    }
}