com.jtechme.apphub.AppDetailsData.java Source code

Java tutorial

Introduction

Here is the source code for com.jtechme.apphub.AppDetailsData.java

Source

/*
 * Copyright (C) 2010-12  Ciaran Gultnieks, ciaran@ciarang.com
 * Copyright (C) 2013-15 Daniel Mart <mvdan@mvdan.cc>
 * Copyright (C) 2013 Stefan Vlkel, bd@bc-bd.org
 * Copyright (C) 2015 Nico Alt, nicoalt@posteo.org
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package com.jtechme.apphub;

import android.app.Activity;
import android.bluetooth.BluetoothAdapter;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.ListFragment;
import android.support.v4.app.NavUtils;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.text.Html;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;

import com.jtechme.apphub.Utils.CommaSeparatedList;
import com.jtechme.apphub.compat.PackageManagerCompat;
import com.jtechme.apphub.data.Apk;
import com.jtechme.apphub.data.ApkProvider;
import com.jtechme.apphub.data.App;
import com.jtechme.apphub.data.AppProvider;
import com.jtechme.apphub.data.Credentials;
import com.jtechme.apphub.data.InstalledAppProvider;
import com.jtechme.apphub.data.Repo;
import com.jtechme.apphub.data.RepoProvider;
import com.jtechme.apphub.installer.Installer;
import com.jtechme.apphub.installer.Installer.AndroidNotCompatibleException;
import com.jtechme.apphub.installer.Installer.InstallerCallback;
import com.jtechme.apphub.net.ApkDownloader;
import com.jtechme.apphub.net.AsyncDownloaderFromAndroid;
import com.jtechme.apphub.net.Downloader;

import java.io.File;
import java.util.Iterator;
import java.util.List;

interface AppDetailsData {
    App getApp();

    AppDetails.ApkListAdapter getApks();
}

/**
 * Interface which allows the apk list fragment to communicate with the activity when
 * a user requests to install/remove an apk by clicking on an item in the list.
 *
 * NOTE: This is <em>not</em> to do with with the sudo/packagemanager/other installer
 * stuff which allows multiple ways to install apps. It is only here to make fragment-
 * activity communication possible.
 */
interface AppInstallListener {
    void install(final Apk apk);

    void removeApk(String packageName);
}

public class AppDetails extends AppCompatActivity implements ProgressListener, AppDetailsData, AppInstallListener {

    private static final String TAG = "AppDetails";

    private static final int REQUEST_ENABLE_BLUETOOTH = 2;

    public static final String EXTRA_APPID = "appid";
    public static final String EXTRA_FROM = "from";
    public static final String EXTRA_HINT_SEARCHING = "searching";

    private FDroidApp fdroidApp;
    private ApkListAdapter adapter;

    private static class ViewHolder {
        TextView version;
        TextView status;
        TextView size;
        TextView api;
        TextView incompatibleReasons;
        TextView buildtype;
        TextView added;
        TextView nativecode;
    }

    // observer to update view when package has been installed/deleted
    private AppObserver myAppObserver;

    class AppObserver extends ContentObserver {

        AppObserver(Handler handler) {
            super(handler);
        }

        @Override
        public void onChange(boolean selfChange) {
            onChange(selfChange, null);
        }

        @Override
        public void onChange(boolean selfChange, Uri uri) {
            onAppChanged();
        }

    }

    class ApkListAdapter extends ArrayAdapter<Apk> {

        private final LayoutInflater mInflater = (LayoutInflater) mctx
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);

        ApkListAdapter(Context context, App app) {
            super(context, 0);
            final List<Apk> apks = ApkProvider.Helper.findByPackageName(context, app.packageName);
            for (final Apk apk : apks) {
                if (apk.compatible || Preferences.get().showIncompatibleVersions()) {
                    add(apk);
                }
            }
        }

        private String getInstalledStatus(final Apk apk) {
            // Definitely not installed.
            if (apk.vercode != app.installedVersionCode) {
                return getString(R.string.app_not_installed);
            }
            // Definitely installed this version.
            if (apk.sig != null && apk.sig.equals(app.installedSig)) {
                return getString(R.string.app_installed);
            }
            // Installed the same version, but from someplace else.
            final String installerPkgName;
            try {
                installerPkgName = mPm.getInstallerPackageName(app.packageName);
            } catch (IllegalArgumentException e) {
                Log.w(TAG, "Application " + app.packageName + " is not installed anymore");
                return getString(R.string.app_not_installed);
            }
            if (TextUtils.isEmpty(installerPkgName)) {
                return getString(R.string.app_inst_unknown_source);
            }
            final String installerLabel = InstalledAppProvider.getApplicationLabel(mctx, installerPkgName);
            return getString(R.string.app_inst_known_source, installerLabel);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {

            java.text.DateFormat df = DateFormat.getDateFormat(mctx);
            final Apk apk = getItem(position);
            ViewHolder holder;

            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.apklistitem, parent, false);

                holder = new ViewHolder();
                holder.version = (TextView) convertView.findViewById(R.id.version);
                holder.status = (TextView) convertView.findViewById(R.id.status);
                holder.size = (TextView) convertView.findViewById(R.id.size);
                holder.api = (TextView) convertView.findViewById(R.id.api);
                holder.incompatibleReasons = (TextView) convertView.findViewById(R.id.incompatible_reasons);
                holder.buildtype = (TextView) convertView.findViewById(R.id.buildtype);
                holder.added = (TextView) convertView.findViewById(R.id.added);
                holder.nativecode = (TextView) convertView.findViewById(R.id.nativecode);

                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }

            holder.version.setText(getString(R.string.version) + " " + apk.version
                    + (apk.vercode == app.suggestedVercode ? "  " : ""));

            holder.status.setText(getInstalledStatus(apk));

            if (apk.size > 0) {
                holder.size.setText(Utils.getFriendlySize(apk.size));
                holder.size.setVisibility(View.VISIBLE);
            } else {
                holder.size.setVisibility(View.GONE);
            }

            if (!Preferences.get().expertMode()) {
                holder.api.setVisibility(View.GONE);
            } else if (apk.minSdkVersion > 0 && apk.maxSdkVersion > 0) {
                holder.api.setText(
                        getString(R.string.minsdk_up_to_maxsdk, Utils.getAndroidVersionName(apk.minSdkVersion),
                                Utils.getAndroidVersionName(apk.maxSdkVersion)));
                holder.api.setVisibility(View.VISIBLE);
            } else if (apk.minSdkVersion > 0) {
                holder.api.setText(
                        getString(R.string.minsdk_or_later, Utils.getAndroidVersionName(apk.minSdkVersion)));
                holder.api.setVisibility(View.VISIBLE);
            } else if (apk.maxSdkVersion > 0) {
                holder.api
                        .setText(getString(R.string.up_to_maxsdk, Utils.getAndroidVersionName(apk.maxSdkVersion)));
                holder.api.setVisibility(View.VISIBLE);
            }

            if (apk.srcname != null) {
                holder.buildtype.setText("source");
            } else {
                holder.buildtype.setText("bin");
            }

            if (apk.added != null) {
                holder.added.setText(getString(R.string.added_on, df.format(apk.added)));
                holder.added.setVisibility(View.VISIBLE);
            } else {
                holder.added.setVisibility(View.GONE);
            }

            if (Preferences.get().expertMode() && apk.nativecode != null) {
                holder.nativecode.setText(apk.nativecode.toString().replaceAll(",", " "));
                holder.nativecode.setVisibility(View.VISIBLE);
            } else {
                holder.nativecode.setVisibility(View.GONE);
            }

            if (apk.incompatibleReasons != null) {
                holder.incompatibleReasons.setText(getResources().getString(R.string.requires_features,
                        apk.incompatibleReasons.toPrettyString()));
                holder.incompatibleReasons.setVisibility(View.VISIBLE);
            } else {
                holder.incompatibleReasons.setVisibility(View.GONE);
            }

            // Disable it all if it isn't compatible...
            final View[] views = { convertView, holder.version, holder.status, holder.size, holder.api,
                    holder.buildtype, holder.added, holder.nativecode, };

            for (final View v : views) {
                v.setEnabled(apk.compatible);
            }

            return convertView;
        }
    }

    private static final int INSTALL = Menu.FIRST;
    private static final int UNINSTALL = Menu.FIRST + 1;
    private static final int IGNOREALL = Menu.FIRST + 2;
    private static final int IGNORETHIS = Menu.FIRST + 3;
    private static final int LAUNCH = Menu.FIRST + 4;
    private static final int SHARE = Menu.FIRST + 5;
    private static final int SEND_VIA_BLUETOOTH = Menu.FIRST + 6;

    private App app;
    private PackageManager mPm;
    private ApkDownloader downloadHandler;
    private LocalBroadcastManager localBroadcastManager;

    private boolean startingIgnoreAll;
    private int startingIgnoreThis;

    private final Context mctx = this;
    private Installer installer;

    private AppDetailsHeaderFragment mHeaderFragment;

    /**
     * Stores relevant data that we want to keep track of when destroying the activity
     * with the expectation of it being recreated straight away (e.g. after an
     * orientation change). One of the major things is that we want the download thread
     * to stay active, but for it not to trigger any UI stuff (e.g. progress bar)
     * between the activity being destroyed and recreated.
     */
    private static class ConfigurationChangeHelper {

        public final ApkDownloader downloader;
        public final App app;

        ConfigurationChangeHelper(ApkDownloader downloader, App app) {
            this.downloader = downloader;
            this.app = app;
        }
    }

    private boolean inProcessOfChangingConfiguration;

    /**
     * Attempt to extract the packageName from the intent which launched this activity.
     * @return May return null, if we couldn't find the packageName. This should
     * never happen as AppDetails is only to be called by the FDroid activity
     * and not externally.
     */
    private String getPackageNameFromIntent(Intent intent) {
        if (!intent.hasExtra(EXTRA_APPID)) {
            Log.e(TAG, "No package name found in the intent!");
            return null;
        }

        return intent.getStringExtra(EXTRA_APPID);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        fdroidApp = (FDroidApp) getApplication();
        fdroidApp.applyTheme(this);

        super.onCreate(savedInstanceState);

        // Must be called *after* super.onCreate(), as that is where the action bar
        // compat implementation is assigned in the ActionBarActivity base class.
        supportRequestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);

        Intent intent = getIntent();
        if (intent.hasExtra(EXTRA_FROM)) {
            setTitle(intent.getStringExtra(EXTRA_FROM));
        }

        mPm = getPackageManager();

        installer = Installer.getActivityInstaller(this, mPm, myInstallerCallback);

        // Get the preferences we're going to use in this Activity...
        ConfigurationChangeHelper previousData = (ConfigurationChangeHelper) getLastCustomNonConfigurationInstance();
        if (previousData != null) {
            Utils.debugLog(TAG, "Recreating view after configuration change.");
            downloadHandler = previousData.downloader;
            if (downloadHandler != null) {
                Utils.debugLog(TAG,
                        "Download was in progress before the configuration change, so we will start to listen to its events again.");
            }
            app = previousData.app;
            setApp(app);
        } else {
            if (!reset(getPackageNameFromIntent(intent))) {
                finish();
                return;
            }
        }

        // Set up the list...
        adapter = new ApkListAdapter(this, app);

        // Wait until all other intialization before doing this, because it will create the
        // fragments, which rely on data from the activity that is set earlier in this method.
        setContentView(R.layout.app_details);

        getSupportActionBar().setDisplayHomeAsUpEnabled(true);

        // Check for the presence of a view which only exists in the landscape view.
        // This seems to be the preferred way to interrogate the view, rather than
        // to check the orientation. I guess this is because views can be dynamically
        // chosen based on more than just orientation (e.g. large screen sizes).
        View onlyInLandscape = findViewById(R.id.app_summary_container);

        AppDetailsListFragment listFragment = (AppDetailsListFragment) getSupportFragmentManager()
                .findFragmentById(R.id.fragment_app_list);
        if (onlyInLandscape == null) {
            listFragment.setupSummaryHeader();
        } else {
            listFragment.removeSummaryHeader();
        }

        localBroadcastManager = LocalBroadcastManager.getInstance(this);

        // Check if a download is running for this app
        if (AsyncDownloaderFromAndroid.isDownloading(this, app.packageName) >= 0) {
            // call install() to re-setup the listeners and downloaders
            // the AsyncDownloader will not restart the download since the download is running,
            // and thus the version we pass to install() is not important
            refreshHeader();
            refreshApkList();
            final Apk apkToInstall = ApkProvider.Helper.find(this, app.packageName, app.suggestedVercode);
            install(apkToInstall);
        }

    }

    @Override
    protected void onStart() {
        super.onStart();
        // register observer to know when install status changes
        myAppObserver = new AppObserver(new Handler());
        getContentResolver().registerContentObserver(AppProvider.getContentUri(app.packageName), true,
                myAppObserver);
    }

    @Override
    protected void onResumeFragments() {
        super.onResumeFragments();
        refreshApkList();
        refreshHeader();
        supportInvalidateOptionsMenu();

        if (downloadHandler != null) {
            if (downloadHandler.isComplete()) {
                downloadCompleteInstallApk();
            } else {
                localBroadcastManager.registerReceiver(downloaderProgressReceiver,
                        new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS));
                downloadHandler.setProgressListener(this);

                if (downloadHandler.getTotalBytes() == 0)
                    mHeaderFragment.startProgress();
                else
                    mHeaderFragment.updateProgress(downloadHandler.getBytesRead(), downloadHandler.getTotalBytes());
            }
        }
    }

    /**
     * Remove progress listener, suppress progress bar, set downloadHandler to null.
     */
    private void cleanUpFinishedDownload() {
        if (downloadHandler != null) {
            downloadHandler.removeProgressListener();
            mHeaderFragment.removeProgress();
            downloadHandler = null;
        }
    }

    /**
     * Once the download completes successfully, call this method to start the install process
     * with the file that was downloaded.
     */
    private void downloadCompleteInstallApk() {
        if (downloadHandler != null) {
            installApk(downloadHandler.localFile());
            cleanUpFinishedDownload();
        }
    }

    protected void onStop() {
        super.onStop();
        getContentResolver().unregisterContentObserver(myAppObserver);
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (app != null
                && (app.ignoreAllUpdates != startingIgnoreAll || app.ignoreThisUpdate != startingIgnoreThis)) {
            Utils.debugLog(TAG, "Updating 'ignore updates', as it has changed since we started the activity...");
            setIgnoreUpdates(app.packageName, app.ignoreAllUpdates, app.ignoreThisUpdate);
        }

        localBroadcastManager.unregisterReceiver(downloaderProgressReceiver);
        if (downloadHandler != null) {
            downloadHandler.removeProgressListener();
        }

        mHeaderFragment.removeProgress();
    }

    private final BroadcastReceiver downloaderProgressReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (mHeaderFragment != null)
                mHeaderFragment.updateProgress(intent.getIntExtra(Downloader.EXTRA_BYTES_READ, -1),
                        intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, -1));
        }
    };

    private void onAppChanged() {
        if (!reset(app.packageName)) {
            AppDetails.this.finish();
            return;
        }

        refreshApkList();
        refreshHeader();
        supportInvalidateOptionsMenu();
    }

    private void setIgnoreUpdates(String packageName, boolean ignoreAll, int ignoreVersionCode) {

        Uri uri = AppProvider.getContentUri(packageName);

        ContentValues values = new ContentValues(2);
        values.put(AppProvider.DataColumns.IGNORE_ALLUPDATES, ignoreAll ? 1 : 0);
        values.put(AppProvider.DataColumns.IGNORE_THISUPDATE, ignoreVersionCode);

        getContentResolver().update(uri, values, null, null);

    }

    @Override
    public Object onRetainCustomNonConfigurationInstance() {
        inProcessOfChangingConfiguration = true;
        return new ConfigurationChangeHelper(downloadHandler, app);
    }

    @Override
    protected void onDestroy() {
        if (downloadHandler != null && !inProcessOfChangingConfiguration) {
            downloadHandler.cancel(false);
            cleanUpFinishedDownload();
        }
        inProcessOfChangingConfiguration = false;
        super.onDestroy();
    }

    // Reset the display and list contents. Used when entering the activity, and
    // also when something has been installed/uninstalled.
    // Return true if the app was found, false otherwise.
    private boolean reset(String packageName) {

        Utils.debugLog(TAG, "Getting application details for " + packageName);
        App newApp = null;

        if (!TextUtils.isEmpty(packageName)) {
            newApp = AppProvider.Helper.findByPackageName(getContentResolver(), packageName);
        }

        setApp(newApp);

        return this.app != null;
    }

    /**
     * If passed null, this will show a message to the user ("Could not find app ..." or something
     * like that) and then finish the activity.
     */
    private void setApp(App newApp) {
        if (newApp == null) {
            Toast.makeText(this, R.string.no_such_app, Toast.LENGTH_LONG).show();
            finish();
            return;
        }

        app = newApp;

        startingIgnoreAll = app.ignoreAllUpdates;
        startingIgnoreThis = app.ignoreThisUpdate;
    }

    private void refreshApkList() {
        adapter.notifyDataSetChanged();
    }

    private void refreshHeader() {
        mHeaderFragment = (AppDetailsHeaderFragment) getSupportFragmentManager().findFragmentById(R.id.header);
        mHeaderFragment.updateViews();
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);
        menu.clear();
        if (app == null)
            return true;

        if (mPm.getLaunchIntentForPackage(app.packageName) != null && app.canAndWantToUpdate()) {
            MenuItemCompat.setShowAsAction(
                    menu.add(Menu.NONE, LAUNCH, 1, R.string.menu_launch).setIcon(R.drawable.ic_play_arrow_white),
                    MenuItemCompat.SHOW_AS_ACTION_IF_ROOM | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
        }

        if (app.isInstalled()) {
            MenuItemCompat.setShowAsAction(
                    menu.add(Menu.NONE, UNINSTALL, 1, R.string.menu_uninstall).setIcon(R.drawable.ic_delete_white),
                    MenuItemCompat.SHOW_AS_ACTION_IF_ROOM | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
        }

        MenuItemCompat.setShowAsAction(
                menu.add(Menu.NONE, SHARE, 1, R.string.menu_share).setIcon(R.drawable.ic_share_white),
                MenuItemCompat.SHOW_AS_ACTION_IF_ROOM | MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);

        menu.add(Menu.NONE, IGNOREALL, 2, R.string.menu_ignore_all).setIcon(R.drawable.ic_do_not_disturb_white)
                .setCheckable(true).setChecked(app.ignoreAllUpdates);

        if (app.hasUpdates()) {
            menu.add(Menu.NONE, IGNORETHIS, 2, R.string.menu_ignore_this)
                    .setIcon(R.drawable.ic_do_not_disturb_white).setCheckable(true)
                    .setChecked(app.ignoreThisUpdate >= app.suggestedVercode);
        }

        // Ignore on devices without Bluetooth
        if (app.isInstalled() && fdroidApp.bluetoothAdapter != null) {
            menu.add(Menu.NONE, SEND_VIA_BLUETOOTH, 3, R.string.send_via_bluetooth)
                    .setIcon(R.drawable.ic_bluetooth_white);
        }
        return true;
    }

    private void tryOpenUri(String s) {
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(s));
        if (intent.resolveActivity(mPm) == null) {
            Toast.makeText(this, getString(R.string.no_handler_app, intent.getDataString()), Toast.LENGTH_LONG)
                    .show();
            return;
        }
        startActivity(intent);
    }

    private static final class SafeLinkMovementMethod extends LinkMovementMethod {

        private static SafeLinkMovementMethod instance;

        private final Context ctx;

        private SafeLinkMovementMethod(Context ctx) {
            this.ctx = ctx;
        }

        public static SafeLinkMovementMethod getInstance(Context ctx) {
            if (instance == null) {
                instance = new SafeLinkMovementMethod(ctx);
            }
            return instance;
        }

        private static CharSequence getLink(TextView widget, Spannable buffer, MotionEvent event) {
            int x = (int) event.getX();
            int y = (int) event.getY();
            x -= widget.getTotalPaddingLeft();
            y -= widget.getTotalPaddingTop();
            x += widget.getScrollX();
            y += widget.getScrollY();

            Layout layout = widget.getLayout();
            final int line = layout.getLineForVertical(y);
            final int off = layout.getOffsetForHorizontal(line, x);
            final ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);

            if (links.length > 0) {
                final ClickableSpan link = links[0];
                final Spanned s = (Spanned) widget.getText();
                return s.subSequence(s.getSpanStart(link), s.getSpanEnd(link));
            }
            return "null";
        }

        @Override
        public boolean onTouchEvent(@NonNull TextView widget, @NonNull Spannable buffer,
                @NonNull MotionEvent event) {
            try {
                return super.onTouchEvent(widget, buffer, event);
            } catch (ActivityNotFoundException ex) {
                Selection.removeSelection(buffer);
                final CharSequence link = getLink(widget, buffer, event);
                Toast.makeText(ctx, ctx.getString(R.string.no_handler_app, link), Toast.LENGTH_LONG).show();
                return true;
            }
        }

    }

    private void navigateUp() {
        NavUtils.navigateUpFromSameTask(this);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {

        switch (item.getItemId()) {

        case android.R.id.home:
            if (getIntent().hasExtra(EXTRA_HINT_SEARCHING)) {
                finish();
            } else {
                navigateUp();
            }
            return true;

        case LAUNCH:
            launchApk(app.packageName);
            return true;

        case SHARE:
            shareApp(app);
            return true;

        case INSTALL:
            // Note that this handles updating as well as installing.
            if (app.suggestedVercode > 0) {
                final Apk apkToInstall = ApkProvider.Helper.find(this, app.packageName, app.suggestedVercode);
                install(apkToInstall);
            }
            return true;

        case UNINSTALL:
            removeApk(app.packageName);
            return true;

        case IGNOREALL:
            app.ignoreAllUpdates ^= true;
            item.setChecked(app.ignoreAllUpdates);
            return true;

        case IGNORETHIS:
            if (app.ignoreThisUpdate >= app.suggestedVercode)
                app.ignoreThisUpdate = 0;
            else
                app.ignoreThisUpdate = app.suggestedVercode;
            item.setChecked(app.ignoreThisUpdate > 0);
            return true;

        case SEND_VIA_BLUETOOTH:
            /*
             * If Bluetooth has not been enabled/turned on, then
             * enabling device discoverability will automatically enable Bluetooth
             */
            Intent discoverBt = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE);
            discoverBt.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 121);
            startActivityForResult(discoverBt, REQUEST_ENABLE_BLUETOOTH);
            // if this is successful, the Bluetooth transfer is started
            return true;

        }
        return super.onOptionsItemSelected(item);
    }

    // Install the version of this app denoted by 'app.curApk'.
    @Override
    public void install(final Apk apk) {
        // Ignore call if another download is running.
        if (downloadHandler != null && !downloadHandler.isComplete())
            return;

        final String repoaddress = getRepoAddress(apk);
        if (repoaddress == null)
            return;

        if (!apk.compatible) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setMessage(R.string.installIncompatible);
            builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int whichButton) {
                    startDownload(apk, repoaddress);
                }
            });
            builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int whichButton) {
                }
            });
            AlertDialog alert = builder.create();
            alert.show();
            return;
        }
        if (app.installedSig != null && apk.sig != null && !apk.sig.equals(app.installedSig)) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setMessage(R.string.SignatureMismatch).setPositiveButton(R.string.ok,
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int id) {
                            dialog.cancel();
                        }
                    });
            AlertDialog alert = builder.create();
            alert.show();
            return;
        }
        startDownload(apk, repoaddress);
    }

    @Nullable
    private String getRepoAddress(Apk apk) {
        final String[] projection = { RepoProvider.DataColumns.ADDRESS };
        Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection);
        if (repo == null || repo.address == null) {
            return null;
        }
        return repo.address;
    }

    @Nullable
    private Credentials getRepoCredentials(Apk apk) {
        final String[] projection = { RepoProvider.DataColumns.USERNAME, RepoProvider.DataColumns.PASSWORD };
        Repo repo = RepoProvider.Helper.findById(this, apk.repo, projection);
        if (repo == null || repo.username == null || repo.password == null) {
            return null;
        }
        return repo.getCredentials();
    }

    private void startDownload(Apk apk, String repoAddress) {
        downloadHandler = new ApkDownloader(getBaseContext(), app, apk, repoAddress);
        downloadHandler.setCredentials(getRepoCredentials(apk));

        localBroadcastManager.registerReceiver(downloaderProgressReceiver,
                new IntentFilter(Downloader.LOCAL_ACTION_PROGRESS));
        downloadHandler.setProgressListener(this);
        if (downloadHandler.download()) {
            mHeaderFragment.startProgress();
        }
    }

    private void installApk(File file) {
        try {
            installer.installPackage(file, app.packageName);
        } catch (AndroidNotCompatibleException e) {
            Log.e(TAG, "Android not compatible with this Installer!", e);
        }
    }

    @Override
    public void removeApk(String packageName) {
        try {
            installer.deletePackage(packageName);
        } catch (AndroidNotCompatibleException e) {
            Log.e(TAG, "Android not compatible with this Installer!", e);
        }
    }

    private final Installer.InstallerCallback myInstallerCallback = new Installer.InstallerCallback() {

        @Override
        public void onSuccess(final int operation) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (operation == Installer.InstallerCallback.OPERATION_INSTALL) {
                        PackageManagerCompat.setInstaller(mPm, app.packageName);
                    }

                    onAppChanged();
                }
            });
        }

        @Override
        public void onError(int operation, final int errorCode) {
            if (errorCode == InstallerCallback.ERROR_CODE_CANCELED) {
                return;
            }
            final int title, body;
            if (operation == InstallerCallback.OPERATION_INSTALL) {
                title = R.string.install_error_title;
                switch (errorCode) {
                case ERROR_CODE_CANNOT_PARSE:
                    body = R.string.install_error_cannot_parse;
                    break;
                default: // ERROR_CODE_OTHER
                    body = R.string.install_error_unknown;
                    break;
                }
            } else { // InstallerCallback.OPERATION_DELETE
                title = R.string.uninstall_error_title;
                switch (errorCode) {
                default: // ERROR_CODE_OTHER
                    body = R.string.uninstall_error_unknown;
                    break;
                }
            }
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    onAppChanged();

                    Log.e(TAG, "Installer aborted with errorCode: " + errorCode);

                    AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
                    alertBuilder.setTitle(title);
                    alertBuilder.setMessage(body);
                    alertBuilder.setNeutralButton(android.R.string.ok, null);
                    alertBuilder.create().show();
                }
            });
        }
    };

    private void launchApk(String packageName) {
        Intent intent = mPm.getLaunchIntentForPackage(packageName);
        startActivity(intent);
    }

    private void shareApp(App app) {
        Intent shareIntent = new Intent(Intent.ACTION_SEND);
        shareIntent.setType("text/plain");

        shareIntent.putExtra(Intent.EXTRA_SUBJECT, app.name);
        shareIntent.putExtra(Intent.EXTRA_TEXT,
                app.name + " (" + app.summary + ") - https://f-droid.org/app/" + app.packageName);

        startActivity(Intent.createChooser(shareIntent, getString(R.string.menu_share)));
    }

    @Override
    public void onProgress(Event event) {
        if (downloadHandler == null || !downloadHandler.isEventFromThis(event)) {
            // Choose not to respond to events from previous downloaders.
            // We don't even care if we receive "cancelled" events or the like, because
            // we dealt with cancellations in the onCancel listener of the dialog,
            // rather than waiting to receive the event here. We try and be careful in
            // the download thread to make sure that we check for cancellations before
            // sending events, but it is not possible to be perfect, because the interruption
            // which triggers the download can happen after the check to see if
            Utils.debugLog(TAG, "Discarding downloader event \"" + event.type
                    + "\" as it is from an old (probably cancelled) downloader.");
            return;
        }

        boolean finished = false;
        switch (event.type) {
        case ApkDownloader.EVENT_ERROR:
            final int res;
            if (event.getData().getInt(ApkDownloader.EVENT_DATA_ERROR_TYPE) == ApkDownloader.ERROR_HASH_MISMATCH)
                res = R.string.corrupt_download;
            else
                res = R.string.details_notinstalled;
            // this must be on the main UI thread
            Toast.makeText(this, res, Toast.LENGTH_LONG).show();
            cleanUpFinishedDownload();
            finished = true;
            break;
        case ApkDownloader.EVENT_APK_DOWNLOAD_COMPLETE:
            downloadCompleteInstallApk();
            finished = true;
            break;
        }

        if (finished) {
            if (mHeaderFragment != null)
                mHeaderFragment.removeProgress();
            downloadHandler = null;
        }
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        // handle cases for install manager first
        if (installer.handleOnActivityResult(requestCode, resultCode, data)) {
            return;
        }

        switch (requestCode) {
        case REQUEST_ENABLE_BLUETOOTH:
            fdroidApp.sendViaBluetooth(this, resultCode, app.packageName);
            break;
        }
    }

    @Override
    public App getApp() {
        return app;
    }

    @Override
    public ApkListAdapter getApks() {
        return adapter;
    }

    public static class AppDetailsSummaryFragment extends Fragment {

        protected final Preferences prefs;
        private AppDetailsData data;
        private static final int MAX_LINES = 5;
        private static boolean viewAllDescription;
        private static LinearLayout llViewMoreDescription;
        private static LinearLayout llViewMorePermissions;
        private final View.OnClickListener expanderPermissions = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final TextView permissionListView = (TextView) llViewMorePermissions
                        .findViewById(R.id.permissions_list);
                final TextView permissionHeader = (TextView) llViewMorePermissions.findViewById(R.id.permissions);

                if (permissionListView.getVisibility() == View.GONE) {
                    permissionListView.setVisibility(View.VISIBLE);
                    permissionHeader.setCompoundDrawablesWithIntrinsicBounds(
                            ContextCompat.getDrawable(getActivity(), R.drawable.ic_lock_24dp_grey600), null,
                            ContextCompat.getDrawable(getActivity(), R.drawable.ic_expand_less_grey600), null);
                } else {
                    permissionListView.setVisibility(View.GONE);
                    permissionHeader.setCompoundDrawablesWithIntrinsicBounds(
                            ContextCompat.getDrawable(getActivity(), R.drawable.ic_lock_24dp_grey600), null,
                            ContextCompat.getDrawable(getActivity(), R.drawable.ic_expand_more_grey600), null);
                }
            }
        };
        private ViewGroup layoutLinks;

        public AppDetailsSummaryFragment() {
            prefs = Preferences.get();
        }

        @Override
        public void onAttach(Activity activity) {
            super.onAttach(activity);
            data = (AppDetailsData) activity;
        }

        protected App getApp() {
            return data.getApp();
        }

        protected ApkListAdapter getApks() {
            return data.getApks();
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            super.onCreateView(inflater, container, savedInstanceState);
            View summaryView = inflater.inflate(R.layout.app_details_summary, container, false);
            setupView(summaryView);
            return summaryView;
        }

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

        // The HTML formatter adds "\n\n" at the end of every paragraph. This
        // is desired between paragraphs, but not at the end of the whole
        // string as it adds unwanted spacing at the end of the TextView.
        // Remove all trailing newlines.
        // Use this function instead of a trim() as that would require
        // converting to String and thus losing formatting (e.g. bold).
        private static CharSequence trimNewlines(CharSequence s) {
            if (s == null || s.length() < 1) {
                return s;
            }
            int i;
            for (i = s.length() - 1; i >= 0; i--) {
                if (s.charAt(i) != '\n') {
                    break;
                }
            }
            if (i == s.length() - 1) {
                return s;
            }
            return s.subSequence(0, i + 1);
        }

        private ViewGroup layoutLinksContent;
        private final View.OnClickListener expanderLinks = new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                TextView linksHeader = (TextView) layoutLinks.findViewById(R.id.information);

                if (layoutLinksContent.getVisibility() == View.GONE) {
                    layoutLinksContent.setVisibility(View.VISIBLE);
                    linksHeader.setCompoundDrawablesWithIntrinsicBounds(
                            ContextCompat.getDrawable(getActivity(), R.drawable.ic_website), null,
                            ContextCompat.getDrawable(getActivity(), R.drawable.ic_expand_less_grey600), null);
                } else {
                    layoutLinksContent.setVisibility(View.GONE);
                    linksHeader.setCompoundDrawablesWithIntrinsicBounds(
                            ContextCompat.getDrawable(getActivity(), R.drawable.ic_website), null,
                            ContextCompat.getDrawable(getActivity(), R.drawable.ic_expand_more_grey600), null);
                }
            }
        };

        private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
            public void onClick(View v) {
                String url = null;
                App app = getApp();
                switch (v.getId()) {
                case R.id.website:
                    url = app.webURL;
                    break;
                case R.id.email:
                    url = "mailto:" + app.email;
                    break;
                case R.id.source:
                    url = app.sourceURL;
                    break;
                case R.id.issues:
                    url = app.trackerURL;
                    break;
                case R.id.changelog:
                    url = app.changelogURL;
                    break;
                case R.id.donate:
                    url = app.donateURL;
                    break;
                case R.id.bitcoin:
                    url = "bitcoin:" + app.bitcoinAddr;
                    break;
                case R.id.litecoin:
                    url = "litecoin:" + app.litecoinAddr;
                    break;
                case R.id.flattr:
                    url = "https://flattr.com/thing/" + app.flattrID;
                    break;
                }
                if (url != null) {
                    ((AppDetails) getActivity()).tryOpenUri(url);
                }
            }
        };

        private final View.OnClickListener expanderDescription = new View.OnClickListener() {
            public void onClick(View v) {
                final TextView description = (TextView) llViewMoreDescription.findViewById(R.id.description);
                final TextView viewMorePermissions = (TextView) llViewMoreDescription
                        .findViewById(R.id.view_more_description);
                if (viewAllDescription) {
                    description.setMaxLines(Integer.MAX_VALUE);
                    viewMorePermissions.setText(getString(R.string.less));
                } else {
                    description.setMaxLines(MAX_LINES);
                    description.setEllipsize(TextUtils.TruncateAt.MARQUEE);
                    viewMorePermissions.setText(R.string.more);
                }
                viewAllDescription ^= true;
            }
        };

        private void setupView(final View view) {
            App app = getApp();
            // Expandable description
            final TextView description = (TextView) view.findViewById(R.id.description);
            final Spanned desc = Html.fromHtml(app.description, null, new Utils.HtmlTagHandler());
            description.setMovementMethod(SafeLinkMovementMethod.getInstance(getActivity()));
            description.setText(trimNewlines(desc));
            final View viewMoreDescription = view.findViewById(R.id.view_more_description);
            description.post(new Runnable() {
                @Override
                public void run() {
                    // If description has more than five lines
                    if (description.getLineCount() > MAX_LINES) {
                        description.setMaxLines(MAX_LINES);
                        description.setEllipsize(TextUtils.TruncateAt.MARQUEE);
                        description.setOnClickListener(expanderDescription);
                        viewAllDescription = true;

                        llViewMoreDescription = (LinearLayout) view.findViewById(R.id.ll_description);
                        llViewMoreDescription.setOnClickListener(expanderDescription);

                        viewMoreDescription.setOnClickListener(expanderDescription);
                    } else {
                        viewMoreDescription.setVisibility(View.GONE);
                    }
                }
            });

            // App ID
            final TextView packageNameView = (TextView) view.findViewById(R.id.package_name);
            if (prefs.expertMode())
                packageNameView.setText(app.packageName);
            else
                packageNameView.setVisibility(View.GONE);

            // Summary
            final TextView summaryView = (TextView) view.findViewById(R.id.summary);
            summaryView.setText(app.summary);

            layoutLinks = (ViewGroup) view.findViewById(R.id.ll_information);
            layoutLinksContent = (ViewGroup) layoutLinks.findViewById(R.id.ll_information_content);

            final TextView linksHeader = (TextView) view.findViewById(R.id.information);
            linksHeader.setOnClickListener(expanderLinks);

            // Website button
            View tv = view.findViewById(R.id.website);
            if (!TextUtils.isEmpty(app.webURL))
                tv.setOnClickListener(mOnClickListener);
            else
                tv.setVisibility(View.GONE);

            // Email button
            tv = view.findViewById(R.id.email);
            if (!TextUtils.isEmpty(app.email))
                tv.setOnClickListener(mOnClickListener);
            else
                tv.setVisibility(View.GONE);

            // Source button
            tv = view.findViewById(R.id.source);
            if (!TextUtils.isEmpty(app.sourceURL))
                tv.setOnClickListener(mOnClickListener);
            else
                tv.setVisibility(View.GONE);

            // Issues button
            tv = view.findViewById(R.id.issues);
            if (!TextUtils.isEmpty(app.trackerURL))
                tv.setOnClickListener(mOnClickListener);
            else
                tv.setVisibility(View.GONE);

            // Changelog button
            tv = view.findViewById(R.id.changelog);
            if (!TextUtils.isEmpty(app.changelogURL))
                tv.setOnClickListener(mOnClickListener);
            else
                tv.setVisibility(View.GONE);

            // Donate button
            tv = view.findViewById(R.id.donate);
            if (!TextUtils.isEmpty(app.donateURL))
                tv.setOnClickListener(mOnClickListener);
            else
                tv.setVisibility(View.GONE);

            // Bitcoin
            tv = view.findViewById(R.id.bitcoin);
            if (!TextUtils.isEmpty(app.bitcoinAddr))
                tv.setOnClickListener(mOnClickListener);
            else
                tv.setVisibility(View.GONE);

            // Litecoin
            tv = view.findViewById(R.id.litecoin);
            if (!TextUtils.isEmpty(app.litecoinAddr))
                tv.setOnClickListener(mOnClickListener);
            else
                tv.setVisibility(View.GONE);

            // Flattr
            tv = view.findViewById(R.id.flattr);
            if (!TextUtils.isEmpty(app.flattrID))
                tv.setOnClickListener(mOnClickListener);
            else
                tv.setVisibility(View.GONE);

            // Categories TextView
            final TextView categories = (TextView) view.findViewById(R.id.categories);
            if (prefs.expertMode() && app.categories != null)
                categories.setText(app.categories.toString().replaceAll(",", ", "));
            else
                categories.setVisibility(View.GONE);

            Apk curApk = null;
            for (int i = 0; i < getApks().getCount(); i++) {
                final Apk apk = getApks().getItem(i);
                if (apk.vercode == app.suggestedVercode) {
                    curApk = apk;
                    break;
                }
            }

            // Expandable permissions
            llViewMorePermissions = (LinearLayout) view.findViewById(R.id.ll_permissions);
            final TextView permissionHeader = (TextView) view.findViewById(R.id.permissions);

            final boolean curApkCompatible = curApk != null && curApk.compatible;
            if (!getApks().isEmpty() && (curApkCompatible || prefs.showIncompatibleVersions())) {
                // build and set the string once
                buildPermissionInfo();
                permissionHeader.setOnClickListener(expanderPermissions);

            } else {
                permissionHeader.setVisibility(View.GONE);
            }

            // Anti features
            final TextView antiFeaturesView = (TextView) view.findViewById(R.id.antifeatures);
            if (app.antiFeatures != null) {
                StringBuilder sb = new StringBuilder();
                for (String af : app.antiFeatures) {
                    String afdesc = descAntiFeature(af);
                    sb.append("\t ").append(afdesc).append('\n');
                }
                if (sb.length() > 0) {
                    sb.setLength(sb.length() - 1);
                    antiFeaturesView.setText(sb.toString());
                } else {
                    antiFeaturesView.setVisibility(View.GONE);
                }
            } else {
                antiFeaturesView.setVisibility(View.GONE);
            }

            updateViews(view);
        }

        private void buildPermissionInfo() {
            final TextView permissionListView = (TextView) llViewMorePermissions
                    .findViewById(R.id.permissions_list);

            CommaSeparatedList permsList = getApks().getItem(0).permissions;
            if (permsList == null) {
                permissionListView.setText(R.string.no_permissions);
            } else {
                Iterator<String> permissions = permsList.iterator();
                StringBuilder sb = new StringBuilder();
                while (permissions.hasNext()) {
                    final String permissionName = permissions.next();
                    try {
                        final Permission permission = new Permission(getActivity(), permissionName);
                        // TODO: Make this list RTL friendly
                        sb.append("\t ").append(permission.getName()).append('\n');
                    } catch (PackageManager.NameNotFoundException e) {
                        Log.e(TAG, "Permission not yet available: " + permissionName);
                    }
                }
                if (sb.length() > 0) {
                    sb.setLength(sb.length() - 1);
                    permissionListView.setText(sb.toString());
                } else {
                    permissionListView.setText(R.string.no_permissions);
                }
            }
        }

        private String descAntiFeature(String af) {
            switch (af) {
            case "Ads":
                return getString(R.string.antiadslist);
            case "Tracking":
                return getString(R.string.antitracklist);
            case "NonFreeNet":
                return getString(R.string.antinonfreenetlist);
            case "NonFreeAdd":
                return getString(R.string.antinonfreeadlist);
            case "NonFreeDep":
                return getString(R.string.antinonfreedeplist);
            case "UpstreamNonFree":
                return getString(R.string.antiupstreamnonfreelist);
            case "NonFreeAssets":
                return getString(R.string.antinonfreeassetslist);
            default:
                return af;
            }
        }

        public void updateViews(View view) {
            if (view == null) {
                Log.e(TAG, "AppDetailsSummaryFragment.updateViews(): view == null. Oops.");
                return;
            }

            App app = getApp();
            TextView signatureView = (TextView) view.findViewById(R.id.signature);
            if (prefs.expertMode() && !TextUtils.isEmpty(app.installedSig)) {
                signatureView.setVisibility(View.VISIBLE);
                signatureView.setText("Signed: " + app.installedSig);
            } else {
                signatureView.setVisibility(View.GONE);
            }
        }
    }

    public static class AppDetailsHeaderFragment extends Fragment implements View.OnClickListener {

        private AppDetailsData data;
        private Button btMain;
        private ProgressBar progressBar;
        private TextView progressSize;
        private TextView progressPercent;
        private ImageButton cancelButton;
        protected final DisplayImageOptions displayImageOptions;
        public static boolean installed;
        public static boolean updateWanted;

        public AppDetailsHeaderFragment() {
            displayImageOptions = new DisplayImageOptions.Builder().cacheInMemory(true).cacheOnDisk(true)
                    .imageScaleType(ImageScaleType.NONE).showImageOnLoading(R.drawable.ic_repo_app_default)
                    .showImageForEmptyUri(R.drawable.ic_repo_app_default).bitmapConfig(Bitmap.Config.RGB_565)
                    .build();
        }

        private App getApp() {
            return data.getApp();
        }

        private ApkListAdapter getApks() {
            return data.getApks();
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            View view = inflater.inflate(R.layout.app_details_header, container, false);
            setupView(view);
            return view;
        }

        @Override
        public void onAttach(Activity activity) {
            super.onAttach(activity);
            data = (AppDetailsData) activity;
        }

        private void setupView(View view) {
            App app = getApp();

            // Set the icon...
            ImageView iv = (ImageView) view.findViewById(R.id.icon);
            ImageLoader.getInstance().displayImage(app.iconUrlLarge, iv, displayImageOptions);

            // Set the title
            TextView tv = (TextView) view.findViewById(R.id.title);
            tv.setText(app.name);

            btMain = (Button) view.findViewById(R.id.btn_main);
            progressBar = (ProgressBar) view.findViewById(R.id.progress_bar);
            progressSize = (TextView) view.findViewById(R.id.progress_size);
            progressPercent = (TextView) view.findViewById(R.id.progress_percentage);
            cancelButton = (ImageButton) view.findViewById(R.id.cancel);
            progressBar.setIndeterminate(false);
            cancelButton.setOnClickListener(this);

            updateViews(view);
        }

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

        /**
         * Displays empty, indeterminate progress bar and related views.
         */
        public void startProgress() {
            setProgressVisible(true);
            progressBar.setIndeterminate(true);
            progressSize.setText("");
            progressPercent.setText("");
            updateViews();
        }

        /**
         * Updates progress bar and captions to new values (in bytes).
         */
        public void updateProgress(long bytesDownloaded, long totalBytes) {
            if (bytesDownloaded < 0 || totalBytes == 0) {
                // Avoid division by zero and other weird values
                return;
            }

            if (totalBytes == -1) {
                setProgressVisible(true);
                progressBar.setIndeterminate(true);
                progressSize.setText(Utils.getFriendlySize(bytesDownloaded));
                progressPercent.setText("");
            } else {
                long percent = bytesDownloaded * 100 / totalBytes;
                setProgressVisible(true);
                progressBar.setIndeterminate(false);
                progressBar.setProgress((int) percent);
                progressBar.setMax(100);
                progressSize.setText(
                        Utils.getFriendlySize(bytesDownloaded) + " / " + Utils.getFriendlySize(totalBytes));
                progressPercent.setText(Long.toString(percent) + " %");
            }
        }

        /**
         * Shows or hides progress bar and related views.
         */
        private void setProgressVisible(boolean visible) {
            int state = visible ? View.VISIBLE : View.GONE;
            progressBar.setVisibility(state);
            progressSize.setVisibility(state);
            progressPercent.setVisibility(state);
            cancelButton.setVisibility(state);
        }

        /**
         * Removes progress bar and related views, invokes {@link #updateViews()}.
         */
        public void removeProgress() {
            setProgressVisible(false);
            updateViews();
        }

        /**
         * Cancels download and hides progress bar.
         */
        @Override
        public void onClick(View view) {
            AppDetails activity = (AppDetails) getActivity();
            if (activity == null || activity.downloadHandler == null)
                return;

            activity.downloadHandler.cancel(true);
            activity.cleanUpFinishedDownload();
            setProgressVisible(false);
            updateViews();
        }

        public void updateViews() {
            updateViews(getView());
        }

        public void updateViews(View view) {
            App app = getApp();
            TextView statusView = (TextView) view.findViewById(R.id.status);
            btMain.setVisibility(View.VISIBLE);

            AppDetails activity = (AppDetails) getActivity();
            if (activity.downloadHandler != null) {
                btMain.setText(R.string.downloading);
                btMain.setEnabled(false);
            } else if (!app.isInstalled() && app.suggestedVercode > 0 && activity.adapter.getCount() > 0) {
                // Check count > 0 due to incompatible apps resulting in an empty list.
                // If App isn't installed
                installed = false;
                statusView.setText(R.string.details_notinstalled);
                NfcHelper.disableAndroidBeam(activity);
                // Set Install button and hide second button
                btMain.setText(R.string.menu_install);
                btMain.setOnClickListener(mOnClickListener);
                btMain.setEnabled(true);
            } else if (app.isInstalled()) {
                // If App is installed
                installed = true;
                statusView.setText(getString(R.string.details_installed, app.installedVersionName));
                NfcHelper.setAndroidBeam(activity, app.packageName);
                if (app.canAndWantToUpdate()) {
                    updateWanted = true;
                    btMain.setText(R.string.menu_upgrade);
                } else {
                    updateWanted = false;
                    if (activity.mPm.getLaunchIntentForPackage(app.packageName) != null) {
                        btMain.setText(R.string.menu_launch);
                    } else {
                        btMain.setText(R.string.menu_uninstall);
                        if (!app.uninstallable) {
                            btMain.setVisibility(View.GONE);
                        }
                    }
                }
                btMain.setOnClickListener(mOnClickListener);
                btMain.setEnabled(true);
            }
            TextView author = (TextView) view.findViewById(R.id.author);
            if (!TextUtils.isEmpty(app.author)) {
                author.setText(getString(R.string.by_author) + " " + app.author);
                author.setVisibility(View.VISIBLE);
            }
            TextView currentVersion = (TextView) view.findViewById(R.id.current_version);
            if (!getApks().isEmpty()) {
                currentVersion.setText(getApks().getItem(0).version + " (" + app.license + ")");
            } else {
                currentVersion.setVisibility(View.GONE);
                btMain.setVisibility(View.GONE);
            }

        }

        private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
            public void onClick(View v) {
                App app = getApp();
                AppDetails activity = (AppDetails) getActivity();
                if (updateWanted && app.suggestedVercode > 0) {
                    Apk apkToInstall = ApkProvider.Helper.find(activity, app.packageName, app.suggestedVercode);
                    activity.install(apkToInstall);
                    return;
                }
                if (installed) {
                    // If installed
                    if (activity.mPm.getLaunchIntentForPackage(app.packageName) != null) {
                        // If "launchable", launch
                        activity.launchApk(app.packageName);
                    } else {
                        activity.removeApk(app.packageName);
                    }
                } else if (app.suggestedVercode > 0) {
                    // If not installed, install
                    btMain.setEnabled(false);
                    btMain.setText(R.string.system_install_installing);
                    final Apk apkToInstall = ApkProvider.Helper.find(activity, app.packageName,
                            app.suggestedVercode);
                    activity.install(apkToInstall);
                }
            }
        };
    }

    public static class AppDetailsListFragment extends ListFragment {

        private static final String SUMMARY_TAG = "summary";

        private AppDetailsData data;
        private AppInstallListener installListener;
        private AppDetailsSummaryFragment summaryFragment;

        private FrameLayout headerView;

        @Override
        public void onAttach(Activity activity) {
            super.onAttach(activity);
            data = (AppDetailsData) activity;
            installListener = (AppInstallListener) activity;
        }

        protected void install(final Apk apk) {
            installListener.install(apk);
        }

        protected void remove() {
            installListener.removeApk(getApp().packageName);
        }

        protected App getApp() {
            return data.getApp();
        }

        protected ApkListAdapter getApks() {
            return data.getApks();
        }

        @Override
        public void onViewCreated(View view, Bundle savedInstanceState) {
            // A bit of a hack, but we can't add the header view in setupSummaryHeader(),
            // due to the fact it needs to happen before setListAdapter(). Also, seeing
            // as we may never add a summary header (i.e. in landscape), this is probably
            // the last opportunity to set the list adapter. As such, we use the headerView
            // as a mechanism to optionally allow adding a header in the future.
            if (headerView == null) {
                headerView = new FrameLayout(getActivity());
                headerView.setId(R.id.appDetailsSummaryHeader);
            } else {
                Fragment summaryFragment = getChildFragmentManager().findFragmentByTag(SUMMARY_TAG);
                if (summaryFragment != null) {
                    getChildFragmentManager().beginTransaction().remove(summaryFragment).commit();
                }
            }

            setListAdapter(null);
            getListView().addHeaderView(headerView);
            setListAdapter(getApks());
        }

        @Override
        public void onListItemClick(ListView l, View v, int position, long id) {
            App app = getApp();
            final Apk apk = getApks().getItem(position - l.getHeaderViewsCount());
            if (app.installedVersionCode == apk.vercode) {
                remove();
            } else if (app.installedVersionCode > apk.vercode) {
                AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
                builder.setMessage(R.string.installDowngrade);
                builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int whichButton) {
                        install(apk);
                    }
                });
                builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int whichButton) {
                    }
                });
                AlertDialog alert = builder.create();
                alert.show();
            } else {
                install(apk);
            }
        }

        public void removeSummaryHeader() {
            Fragment summary = getChildFragmentManager().findFragmentByTag(SUMMARY_TAG);
            if (summary != null) {
                getChildFragmentManager().beginTransaction().remove(summary).commit();
                headerView.removeAllViews();
                headerView.setVisibility(View.GONE);
                summaryFragment = null;
            }
        }

        public void setupSummaryHeader() {
            Fragment fragment = getChildFragmentManager().findFragmentByTag(SUMMARY_TAG);
            if (fragment != null) {
                summaryFragment = (AppDetailsSummaryFragment) fragment;
            } else {
                summaryFragment = new AppDetailsSummaryFragment();
            }
            getChildFragmentManager().beginTransaction().replace(headerView.getId(), summaryFragment, SUMMARY_TAG)
                    .commit();
            headerView.setVisibility(View.VISIBLE);
        }
    }

}