BobStore/src/org/fdroid/fdroid/AppDetails.java

1098 lines
39 KiB
Java

/*
* Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com
* Copyright (C) 2013 Stefan Völkel, bd@bc-bd.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 org.fdroid.fdroid;
import java.io.File;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.xml.sax.XMLReader;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.app.ProgressDialog;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import android.content.pm.PackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.Signature;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.text.Editable;
import android.text.Html;
import android.text.Html.TagHandler;
import android.text.Spanned;
import android.text.format.DateFormat;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.graphics.Bitmap;
import android.support.v4.app.NavUtils;
import android.support.v4.view.MenuItemCompat;
import org.fdroid.fdroid.compat.PackageManagerCompat;
import org.fdroid.fdroid.compat.ActionBarCompat;
import org.fdroid.fdroid.compat.MenuManager;
import org.fdroid.fdroid.DB.CommaSeparatedList;
import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
import com.nostra13.universalimageloader.utils.StorageUtils;
import android.os.Environment;
public class AppDetails extends ListActivity {
private static final int REQUEST_INSTALL = 0;
private static final int REQUEST_UNINSTALL = 1;
private static class ViewHolder {
TextView version;
TextView status;
TextView size;
TextView api;
TextView buildtype;
TextView added;
TextView nativecode;
}
private class ApkListAdapter extends BaseAdapter {
private List<DB.Apk> items;
private LayoutInflater mInflater;
public ApkListAdapter(Context context, List<DB.Apk> items) {
this.items = new ArrayList<DB.Apk>();
if (items != null) {
for (DB.Apk apk : items) {
this.addItem(apk);
}
}
mInflater = (LayoutInflater) mctx.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
public void addItem(DB.Apk apk) {
if (apk.compatible || pref_incompatibleVersions) {
items.add(apk);
}
}
public List<DB.Apk> getItems() {
return items;
}
@Override
public int getCount() {
return items.size();
}
@Override
public Object getItem(int position) {
return items.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
java.text.DateFormat df = DateFormat.getDateFormat(mctx);
DB.Apk apk = items.get(position);
ViewHolder holder;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.apklistitem, null);
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.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 == app.curApk ? "" : ""));
if (apk.vercode == app.installedVerCode
&& apk.sig.equals(mInstalledSigID)) {
holder.status.setText(getString(R.string.inst));
} else {
holder.status.setText(getString(R.string.not_inst));
}
if (apk.detail_size > 0) {
holder.size.setText(Utils.getFriendlySize(apk.detail_size));
} else {
holder.size.setText("");
}
if (apk.minSdkVersion > 0) {
holder.api.setText(getString(R.string.minsdk_or_later,
Utils.getAndroidVersionName(apk.minSdkVersion)));
holder.api.setEnabled(apk.compatible);
holder.api.setVisibility(View.VISIBLE);
} else {
holder.api.setVisibility(View.GONE);
}
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)));
} else {
holder.added.setText("");
}
if (pref_expert && apk.nativecode != null) {
holder.nativecode.setText(apk.nativecode.toString().replaceAll(","," "));
} else {
holder.nativecode.setText("");
}
// Disable it all if it isn't compatible...
View[] views = {
convertView,
holder.version,
holder.status,
holder.size,
holder.api,
holder.buildtype,
holder.added,
holder.nativecode
};
for (View view : views) {
view.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 WEBSITE = Menu.FIRST + 4;
private static final int ISSUES = Menu.FIRST + 5;
private static final int SOURCE = Menu.FIRST + 6;
private static final int LAUNCH = Menu.FIRST + 7;
private static final int SHARE = Menu.FIRST + 8;
private static final int DONATE = Menu.FIRST + 9;
private static final int BITCOIN = Menu.FIRST + 10;
private static final int LITECOIN = Menu.FIRST + 11;
private static final int DOGECOIN = Menu.FIRST + 12;
private static final int FLATTR = Menu.FIRST + 13;
private static final int DONATE_URL = Menu.FIRST + 14;
private DB.App app;
private String appid;
private PackageManager mPm;
private DownloadHandler downloadHandler;
private boolean stateRetained;
private boolean startingIgnoreAll;
private int startingIgnoreThis;
LinearLayout headerView;
View infoView;
private final Context mctx = this;
private DisplayImageOptions displayImageOptions;
@Override
protected void onCreate(Bundle savedInstanceState) {
((FDroidApp) getApplication()).applyTheme(this);
super.onCreate(savedInstanceState);
displayImageOptions = new DisplayImageOptions.Builder()
.cacheInMemory(true)
.cacheOnDisc(true)
.imageScaleType(ImageScaleType.NONE)
.showImageOnLoading(R.drawable.ic_repo_app_default)
.showImageForEmptyUri(R.drawable.ic_repo_app_default)
.bitmapConfig(Bitmap.Config.RGB_565)
.build();
ActionBarCompat abCompat = ActionBarCompat.create(this);
abCompat.setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.appdetails);
Intent i = getIntent();
Uri data = i.getData();
if (data != null) {
if (data.isHierarchical()) {
if (data.getHost() != null && data.getHost().equals("details")) {
// market://details?id=app.id
appid = data.getQueryParameter("id");
} else {
// https://f-droid.org/app/app.id
appid = data.getLastPathSegment();
if (appid != null && appid.equals("app")) appid = null;
}
} else {
// fdroid.app:app.id
appid = data.getEncodedSchemeSpecificPart();
}
Log.d("FDroid", "AppDetails launched from link, for '" + appid
+ "'");
} else if (!i.hasExtra("appid")) {
Log.d("FDroid", "No application ID in AppDetails!?");
} else {
appid = i.getStringExtra("appid");
}
if (i.hasExtra("from")) {
setTitle(i.getStringExtra("from"));
}
mPm = getPackageManager();
// Get the preferences we're going to use in this Activity...
AppDetails old = (AppDetails) getLastNonConfigurationInstance();
if (old != null) {
copyState(old);
} else {
if (!reset()) {
finish();
return;
}
resetRequired = false;
}
// Set up the list...
headerView = new LinearLayout(this);
ListView lv = (ListView) findViewById(android.R.id.list);
lv.addHeaderView(headerView);
ApkListAdapter la = new ApkListAdapter(this, app.apks);
setListAdapter(la);
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(getBaseContext());
pref_smallDensity = prefs.getBoolean("smallDensity", false);
pref_expert = prefs.getBoolean("expert", false);
pref_permissions = prefs.getBoolean("showPermissions", false);
pref_incompatibleVersions = prefs.getBoolean(
"incompatibleVersions", false);
startViews();
}
private boolean pref_smallDensity;
private boolean pref_expert;
private boolean pref_permissions;
private boolean pref_incompatibleVersions;
private boolean resetRequired;
// The signature of the installed version.
private Signature mInstalledSignature;
private String mInstalledSigID;
@Override
protected void onResume() {
super.onResume();
if (resetRequired) {
if (!reset()) {
finish();
return;
}
resetRequired = false;
}
updateViews();
MenuManager.create(this).invalidateOptionsMenu();
if (downloadHandler != null) {
downloadHandler.startUpdates();
}
}
@Override
protected void onPause() {
if (downloadHandler != null) {
downloadHandler.stopUpdates();
}
if (app != null && (app.ignoreAllUpdates != startingIgnoreAll
|| app.ignoreThisUpdate != startingIgnoreThis)) {
try {
DB db = DB.getDB();
db.setIgnoreUpdates(app.id,
app.ignoreAllUpdates, app.ignoreThisUpdate);
} finally {
DB.releaseDB();
}
}
super.onPause();
}
@Override
public Object onRetainNonConfigurationInstance() {
stateRetained = true;
return this;
}
@Override
protected void onDestroy() {
if (downloadHandler != null) {
if (!stateRetained)
downloadHandler.cancel();
downloadHandler.destroy();
}
super.onDestroy();
}
// Copy all relevant state from an old instance. This is used in
// place of reset(), so it must initialize all fields normally set
// there.
private void copyState(AppDetails old) {
if (old.downloadHandler != null)
downloadHandler = new DownloadHandler(old.downloadHandler);
app = old.app;
mInstalledSignature = old.mInstalledSignature;
mInstalledSigID = old.mInstalledSigID;
}
// 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() {
Log.d("FDroid", "Getting application details for " + appid);
app = null;
if (appid != null && appid.length() > 0) {
List<DB.App> apps = ((FDroidApp) getApplication()).getApps();
for (DB.App tapp : apps) {
if (tapp.id.equals(appid)) {
app = tapp;
break;
}
}
}
if (app == null) {
Toast toast = Toast.makeText(this,
getString(R.string.no_such_app), Toast.LENGTH_LONG);
toast.show();
finish();
return false;
}
// Make sure the app is populated.
try {
DB db = DB.getDB();
db.populateDetails(app, 0);
} catch (Exception ex) {
Log.d("FDroid", "Failed to populate app - " + ex.getMessage());
} finally {
DB.releaseDB();
}
startingIgnoreAll = app.ignoreAllUpdates;
startingIgnoreThis = app.ignoreThisUpdate;
// Get the signature of the installed package...
mInstalledSignature = null;
mInstalledSigID = null;
if (app.installedVersion != null) {
PackageManager pm = getBaseContext().getPackageManager();
try {
PackageInfo pi = pm.getPackageInfo(appid,
PackageManager.GET_SIGNATURES);
mInstalledSignature = pi.signatures[0];
Hasher hash = new Hasher("MD5", mInstalledSignature
.toCharsString().getBytes());
mInstalledSigID = hash.getHash();
} catch (NameNotFoundException e) {
Log.d("FDroid", "Failed to get installed signature");
} catch (NoSuchAlgorithmException e) {
Log.d("FDroid", "Failed to calculate signature MD5 sum");
mInstalledSignature = null;
}
}
return true;
}
private void startViews() {
// Insert the 'infoView' (which contains the summary, various odds and
// ends, and the description) into the appropriate place, if we're in
// landscape mode. In portrait mode, we put it in the listview's
// header..
infoView = View.inflate(this, R.layout.appinfo, null);
LinearLayout landparent = (LinearLayout) findViewById(R.id.landleft);
headerView.removeAllViews();
if (landparent != null) {
landparent.addView(infoView);
Log.d("FDroid", "Setting landparent infoview");
} else {
headerView.addView(infoView);
Log.d("FDroid", "Setting header infoview");
}
// Set the icon...
ImageView iv = (ImageView) findViewById(R.id.icon);
ImageLoader.getInstance().displayImage(app.iconUrl, iv,
displayImageOptions);
// Set the title and other header details...
TextView tv = (TextView) findViewById(R.id.title);
tv.setText(app.name);
tv = (TextView) findViewById(R.id.license);
tv.setText(app.license);
if (app.categories != null) {
tv = (TextView) findViewById(R.id.categories);
tv.setText(app.categories.toString().replaceAll(",",", "));
}
tv = (TextView) infoView.findViewById(R.id.description);
tv.setMovementMethod(LinkMovementMethod.getInstance());
// Need this to add the unimplemented support for ordered and unordered
// lists to Html.fromHtml().
class HtmlTagHandler implements TagHandler {
int listNum;
@Override
public void handleTag(boolean opening, String tag, Editable output,
XMLReader reader) {
if (tag.equals("ul")) {
if (opening)
listNum = -1;
else
output.append('\n');
} else if (opening && tag.equals("ol")) {
if (opening)
listNum = 1;
else
output.append('\n');
} else if (tag.equals("li")) {
if (opening) {
if (listNum == -1) {
output.append("\t• ");
} else {
output.append("\t").append(Integer.toString(listNum)).append(". ");
listNum++;
}
} else {
output.append('\n');
}
}
}
}
Spanned desc = Html.fromHtml(
app.detail_description, null, new HtmlTagHandler());
tv.setText(desc.subSequence(0, desc.length() - 2));
tv = (TextView) infoView.findViewById(R.id.appid);
if (pref_expert)
tv.setText(app.id);
else
tv.setVisibility(View.GONE);
tv = (TextView) infoView.findViewById(R.id.summary);
tv.setText(app.summary);
if (pref_permissions && !app.apks.isEmpty()) {
tv = (TextView) infoView.findViewById(R.id.permissions_list);
CommaSeparatedList permsList = app.apks.get(0).detail_permissions;
if (permsList == null) {
tv.setText(getString(R.string.no_permissions));
} else {
Iterator<String> permissions = permsList.iterator();
StringBuilder sb = new StringBuilder();
while (permissions.hasNext()) {
String permissionName = permissions.next();
try {
Permission permission = new Permission(this, permissionName);
sb.append("\t• ").append(permission.getName()).append('\n');
} catch (NameNotFoundException e) {
if (permissionName.equals("ACCESS_SUPERUSER")) {
sb.append("\t• Full permissions to all device features and storage\n");
} else {
Log.d("FDroid", "Permission not yet available: "
+permissionName);
}
}
}
if (sb.length() > 0) sb.setLength(sb.length() - 1);
tv.setText(sb.toString());
}
tv = (TextView) infoView.findViewById(R.id.permissions);
tv.setText(getString(
R.string.permissions_for_long, app.apks.get(0).version));
} else {
infoView.findViewById(R.id.permissions).setVisibility(View.GONE);
infoView.findViewById(R.id.permissions_list).setVisibility(View.GONE);
}
tv = (TextView) infoView.findViewById(R.id.antifeatures);
if (app.antiFeatures != null) {
StringBuilder sb = new StringBuilder();
for (String af : app.antiFeatures) {
String afdesc = descAntiFeature(af);
if (afdesc != null) {
sb.append("\t• ").append(afdesc).append("\n");
}
}
if (sb.length() > 0) {
sb.setLength(sb.length() - 1);
tv.setText(sb.toString());
} else {
tv.setVisibility(View.GONE);
}
} else {
tv.setVisibility(View.GONE);
}
}
private String descAntiFeature(String af) {
if (af.equals("Ads"))
return getString(R.string.antiadslist);
if (af.equals("Tracking"))
return getString(R.string.antitracklist);
if (af.equals("NonFreeNet"))
return getString(R.string.antinonfreenetlist);
if (af.equals("NonFreeAdd"))
return getString(R.string.antinonfreeadlist);
if (af.equals("NonFreeDep"))
return getString(R.string.antinonfreedeplist);
if (af.equals("UpstreamNonFree"))
return getString(R.string.antiupstreamnonfreelist);
return null;
}
private void updateViews() {
// Refresh the list...
ApkListAdapter la = (ApkListAdapter) getListAdapter();
la.notifyDataSetChanged();
TextView tv = (TextView) findViewById(R.id.status);
if (app.installedVersion == null)
tv.setText(getString(R.string.details_notinstalled));
else
tv.setText(getString(R.string.details_installed,
app.installedVersion));
tv = (TextView) infoView.findViewById(R.id.signature);
if (pref_expert && mInstalledSignature != null) {
tv.setVisibility(View.VISIBLE);
tv.setText("Signed: " + mInstalledSigID);
} else {
tv.setVisibility(View.GONE);
}
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
app.curApk = app.apks.get(position - l.getHeaderViewsCount());
if (app.installedVerCode == app.curApk.vercode)
removeApk(app.id);
else if (app.installedVerCode > app.curApk.vercode) {
AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
ask_alrt.setMessage(getString(R.string.installDowngrade));
ask_alrt.setPositiveButton(getString(R.string.yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int whichButton) {
install();
}
});
ask_alrt.setNegativeButton(getString(R.string.no),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int whichButton) {
}
});
AlertDialog alert = ask_alrt.create();
alert.show();
} else
install();
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
menu.clear();
if (app == null)
return true;
if (app.toUpdate) {
MenuItemCompat.setShowAsAction(menu.add(
Menu.NONE, INSTALL, 0, R.string.menu_upgrade)
.setIcon(R.drawable.ic_menu_refresh),
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
}
if (app.installedVersion == null && app.curApk != null) {
MenuItemCompat.setShowAsAction(menu.add(
Menu.NONE, INSTALL, 1, R.string.menu_install)
.setIcon(android.R.drawable.ic_menu_add),
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
} else if (app.installedVersion != null) {
MenuItemCompat.setShowAsAction(menu.add(
Menu.NONE, UNINSTALL, 1, R.string.menu_uninstall)
.setIcon(android.R.drawable.ic_menu_delete),
MenuItemCompat.SHOW_AS_ACTION_IF_ROOM |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
if (mPm.getLaunchIntentForPackage(app.id) != null) {
MenuItemCompat.setShowAsAction(menu.add(
Menu.NONE, LAUNCH, 1, R.string.menu_launch)
.setIcon(android.R.drawable.ic_media_play),
MenuItemCompat.SHOW_AS_ACTION_ALWAYS |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
}
}
MenuItemCompat.setShowAsAction(menu.add(
Menu.NONE, SHARE, 1, R.string.menu_share)
.setIcon(android.R.drawable.ic_menu_share),
MenuItemCompat.SHOW_AS_ACTION_IF_ROOM |
MenuItemCompat.SHOW_AS_ACTION_WITH_TEXT);
menu.add(Menu.NONE, IGNOREALL, 2, R.string.menu_ignore_all)
.setIcon(android.R.drawable.ic_menu_close_clear_cancel)
.setCheckable(true)
.setChecked(app.ignoreAllUpdates);
if (app.hasUpdates) {
menu.add(Menu.NONE, IGNORETHIS, 2, R.string.menu_ignore_this)
.setIcon(android.R.drawable.ic_menu_close_clear_cancel)
.setCheckable(true)
.setChecked(app.ignoreThisUpdate >= app.curApk.vercode);
}
if (app.detail_webURL.length() > 0) {
menu.add(Menu.NONE, WEBSITE, 3, R.string.menu_website).setIcon(
android.R.drawable.ic_menu_view);
}
if (app.detail_trackerURL.length() > 0) {
menu.add(Menu.NONE, ISSUES, 4, R.string.menu_issues).setIcon(
android.R.drawable.ic_menu_view);
}
if (app.detail_sourceURL.length() > 0) {
menu.add(Menu.NONE, SOURCE, 5, R.string.menu_source).setIcon(
android.R.drawable.ic_menu_view);
}
if (app.detail_bitcoinAddr != null || app.detail_litecoinAddr != null ||
app.detail_dogecoinAddr != null ||
app.detail_flattrID != null || app.detail_donateURL != null) {
SubMenu donate = menu.addSubMenu(Menu.NONE, DONATE, 7,
R.string.menu_donate).setIcon(
android.R.drawable.ic_menu_send);
if (app.detail_bitcoinAddr != null)
donate.add(Menu.NONE, BITCOIN, 8, R.string.menu_bitcoin);
if (app.detail_litecoinAddr != null)
donate.add(Menu.NONE, LITECOIN, 8, R.string.menu_litecoin);
if (app.detail_dogecoinAddr != null)
donate.add(Menu.NONE, DOGECOIN, 8, R.string.menu_dogecoin);
if (app.detail_flattrID != null)
donate.add(Menu.NONE, FLATTR, 9, R.string.menu_flattr);
if (app.detail_donateURL != null)
donate.add(Menu.NONE, DONATE_URL, 10, R.string.menu_website);
}
return true;
}
public void tryOpenUri(String s) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(s));
if (intent.resolveActivity(getPackageManager()) == null) {
Toast.makeText(this,
getString(R.string.no_handler_app, intent.getDataString()),
Toast.LENGTH_LONG).show();
return;
}
startActivity(intent);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
case LAUNCH:
launchApk(app.id);
return true;
case SHARE:
shareApp(app);
return true;
case INSTALL:
// Note that this handles updating as well as installing.
if (app.curApk != null)
install();
return true;
case UNINSTALL:
removeApk(app.id);
return true;
case IGNOREALL:
app.ignoreAllUpdates ^= true;
item.setChecked(app.ignoreAllUpdates);
return true;
case IGNORETHIS:
if (app.ignoreThisUpdate >= app.curApk.vercode)
app.ignoreThisUpdate = 0;
else
app.ignoreThisUpdate = app.curApk.vercode;
item.setChecked(app.ignoreThisUpdate > 0);
return true;
case WEBSITE:
tryOpenUri(app.detail_webURL);
return true;
case ISSUES:
tryOpenUri(app.detail_trackerURL);
return true;
case SOURCE:
tryOpenUri(app.detail_sourceURL);
return true;
case BITCOIN:
tryOpenUri("bitcoin:" + app.detail_bitcoinAddr);
return true;
case LITECOIN:
tryOpenUri("litecoin:" + app.detail_litecoinAddr);
return true;
case DOGECOIN:
tryOpenUri("dogecoin:" + app.detail_dogecoinAddr);
return true;
case FLATTR:
tryOpenUri("https://flattr.com/thing/" + app.detail_flattrID);
return true;
case DONATE_URL:
tryOpenUri(app.detail_donateURL);
return true;
}
return super.onOptionsItemSelected(item);
}
// Install the version of this app denoted by 'app.curApk'.
private void install() {
String ra = null;
try {
DB db = DB.getDB();
DB.Repo repo = db.getRepo(app.curApk.repo);
if (repo != null)
ra = repo.address;
} catch (Exception ex) {
Log.d("FDroid", "Failed to get repo address - " + ex.getMessage());
} finally {
DB.releaseDB();
}
if (ra == null)
return;
final String repoaddress = ra;
if (!app.curApk.compatible) {
AlertDialog.Builder ask_alrt = new AlertDialog.Builder(this);
ask_alrt.setMessage(getString(R.string.installIncompatible));
ask_alrt.setPositiveButton(getString(R.string.yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int whichButton) {
downloadHandler = new DownloadHandler(app.curApk,
repoaddress, Utils
.getApkCacheDir(getBaseContext()));
}
});
ask_alrt.setNegativeButton(getString(R.string.no),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int whichButton) {
}
});
AlertDialog alert = ask_alrt.create();
alert.show();
return;
}
if (mInstalledSigID != null && app.curApk.sig != null
&& !app.curApk.sig.equals(mInstalledSigID)) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(R.string.SignatureMismatch).setPositiveButton(
getString(R.string.ok),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
AlertDialog alert = builder.create();
alert.show();
return;
}
downloadHandler = new DownloadHandler(app.curApk, repoaddress,
Utils.getApkCacheDir(getBaseContext()));
}
private void removeApk(String id) {
PackageInfo pkginfo;
try {
pkginfo = mPm.getPackageInfo(id, 0);
} catch (NameNotFoundException e) {
Log.d("FDroid", "Couldn't find package " + id + " to uninstall.");
return;
}
Uri uri = Uri.fromParts("package", pkginfo.packageName, null);
Intent intent = new Intent(Intent.ACTION_DELETE, uri);
startActivityForResult(intent, REQUEST_UNINSTALL);
((FDroidApp) getApplication()).invalidateApp(id);
}
private void installApk(File file, String id) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file://" + file.getPath()),
"application/vnd.android.package-archive");
startActivityForResult(intent, REQUEST_INSTALL);
((FDroidApp) getApplication()).invalidateApp(id);
}
private void launchApk(String id) {
Intent intent = mPm.getLaunchIntentForPackage(id);
startActivity(intent);
}
private void shareApp(DB.App app) {
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Android App: "+app.name);
shareIntent.putExtra(Intent.EXTRA_TEXT, app.name+" ("+app.summary+") - https://f-droid.org/app/"+app.id);
startActivity(Intent.createChooser(shareIntent, getString(R.string.menu_share)));
}
private ProgressDialog createProgressDialog(String file, int p, int max) {
final ProgressDialog pd = new ProgressDialog(this);
pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
pd.setMessage(getString(R.string.download_server) + ":\n " + file);
pd.setMax(max);
pd.setProgress(p);
pd.setCancelable(true);
pd.setCanceledOnTouchOutside(false);
pd.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
downloadHandler.cancel();
}
});
pd.setButton(DialogInterface.BUTTON_NEUTRAL,
getString(R.string.cancel),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
pd.cancel();
}
});
pd.show();
return pd;
}
// Handler used to update the progress dialog while downloading.
private class DownloadHandler extends Handler {
private Downloader download;
private ProgressDialog pd;
private boolean updating;
private String id;
public DownloadHandler(DB.Apk apk, String repoaddress, File destdir) {
id = apk.id;
download = new Downloader(apk, repoaddress, destdir);
download.start();
startUpdates();
}
public DownloadHandler(DownloadHandler oldHandler) {
if (oldHandler != null) {
download = oldHandler.download;
}
startUpdates();
}
public boolean updateProgress() {
boolean finished = false;
switch (download.getStatus()) {
case RUNNING:
if (pd == null) {
pd = createProgressDialog(download.remoteFile(),
download.getProgress(), download.getMax());
} else {
pd.setProgress(download.getProgress());
}
break;
case ERROR:
if (pd != null)
pd.dismiss();
String text;
if (download.getErrorType() == Downloader.Error.CORRUPT)
text = getString(R.string.corrupt_download);
else
text = download.getErrorMessage();
Toast.makeText(AppDetails.this, text, Toast.LENGTH_LONG).show();
finished = true;
break;
case DONE:
if (pd != null)
pd.dismiss();
installApk(download.localFile(), id);
finished = true;
break;
case CANCELLED:
Toast.makeText(AppDetails.this,
getString(R.string.download_cancelled),
Toast.LENGTH_SHORT).show();
finished = true;
break;
default:
break;
}
return finished;
}
public void startUpdates() {
if (!updating) {
updating = true;
sendEmptyMessage(0);
}
}
public void stopUpdates() {
updating = false;
removeMessages(0);
}
public void cancel() {
if (download != null)
download.interrupt();
}
public void destroy() {
// The dialog can't be dismissed when it's not displayed,
// so do it when the activity is being destroyed.
if (pd != null) {
pd.dismiss();
pd = null;
}
// Cancel any scheduled updates so that we don't
// accidentally recreate the progress dialog.
stopUpdates();
}
// Repeatedly run updateProgress() until it's finished.
@Override
public void handleMessage(Message msg) {
if (download == null)
return;
boolean finished = updateProgress();
if (finished)
download = null;
else
sendMessageDelayed(obtainMessage(), 50);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_INSTALL:
if (downloadHandler != null) {
downloadHandler = null;
}
PackageManagerCompat.setInstaller(mPm, app.id);
resetRequired = true;
break;
case REQUEST_UNINSTALL:
resetRequired = true;
break;
}
}
}