/*
* Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com
* Copyright (C) 2013-15 Daniel Martí <mvdan@mvdan.cc>
* Copyright (C) 2013 Stefan Völkel, 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 org.fdroid.fdroid;
import android.app.Activity;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.NonNull;
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 org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppPrefs;
import org.fdroid.fdroid.data.AppPrefsProvider;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.InstalledApp;
import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.InstallerFactory;
import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.net.Downloader;
import org.fdroid.fdroid.net.DownloaderService;
import org.fdroid.fdroid.privileged.views.AppDiff;
import org.fdroid.fdroid.privileged.views.AppSecurityPermissions;
import java.util.List;
public class AppDetails extends AppCompatActivity {
private static final String TAG = "AppDetails";
private static final int REQUEST_ENABLE_BLUETOOTH = 2;
private static final int REQUEST_PERMISSION_DIALOG = 3;
private static final int REQUEST_UNINSTALL_DIALOG = 4;
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;
/**
* Check if {@code packageName} is currently visible to the user.
*/
public static boolean isAppVisible(String packageName) {
return packageName != null && packageName.equals(visiblePackageName);
}
private static String visiblePackageName;
private static class ViewHolder {
TextView version;
TextView status;
TextView repository;
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 inflater = (LayoutInflater) context.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.versionCode != 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 = packageManager.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(context, 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(context);
final Apk apk = getItem(position);
ViewHolder holder;
if (convertView == null) {
convertView = inflater.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.repository = (TextView) convertView.findViewById(R.id.repository);
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.versionName
+ (apk.versionCode == app.suggestedVersionCode ? " ☆" : ""));
holder.status.setText(getInstalledStatus(apk));
holder.repository.setText(getString(R.string.repo_provider,
RepoProvider.Helper.findById(getContext(), apk.repo).getName()));
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 < Apk.SDK_VERSION_MAX_VALUE) {
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(TextUtils.join(" ", apk.nativecode));
holder.nativecode.setVisibility(View.VISIBLE);
} else {
holder.nativecode.setVisibility(View.GONE);
}
if (apk.incompatibleReasons != null) {
holder.incompatibleReasons.setText(
getResources().getString(
R.string.requires_features,
TextUtils.join(", ", apk.incompatibleReasons)));
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.repository,
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 packageManager;
private String activeDownloadUrlString;
private LocalBroadcastManager localBroadcastManager;
private AppPrefs startingPrefs;
private final Context context = this;
private AppDetailsHeaderFragment headerFragment;
/**
* 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 String urlString;
public final App app;
ConfigurationChangeHelper(String urlString, App app) {
this.urlString = urlString;
this.app = app;
}
}
/**
* 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));
}
packageManager = getPackageManager();
// 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.");
activeDownloadUrlString = previousData.urlString;
if (activeDownloadUrlString != 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);
}
@Override
protected void onStart() {
super.onStart();
// register observer to know when install status changes
myAppObserver = new AppObserver(new Handler());
getContentResolver().registerContentObserver(
AppProvider.getHighestPriorityMetadataUri(app.packageName),
true,
myAppObserver);
}
@Override
protected void onResumeFragments() {
// Must be called before super.onResumeFragments(), as the fragments depend on the active
// url being correctly set in order to know whether or not to show the download progress bar.
calcActiveDownloadUrlString(app.packageName);
super.onResumeFragments();
headerFragment = (AppDetailsHeaderFragment) getSupportFragmentManager().findFragmentById(R.id.header);
refreshApkList();
supportInvalidateOptionsMenu();
if (DownloaderService.isQueuedOrActive(activeDownloadUrlString)) {
registerDownloaderReceiver();
}
visiblePackageName = app.packageName;
}
/**
* Remove progress listener, suppress progress bar, set downloadHandler to null.
*/
private void cleanUpFinishedDownload() {
activeDownloadUrlString = null;
if (headerFragment != null) {
headerFragment.removeProgress();
}
unregisterDownloaderReceiver();
}
protected void onStop() {
super.onStop();
getContentResolver().unregisterContentObserver(myAppObserver);
}
@Override
protected void onPause() {
super.onPause();
visiblePackageName = null;
// save the active URL for this app in case we come back
getPreferences(MODE_PRIVATE)
.edit()
.putString(getPackageNameFromIntent(getIntent()), activeDownloadUrlString)
.apply();
if (app != null && !app.getPrefs(this).equals(startingPrefs)) {
Utils.debugLog(TAG, "Updating 'ignore updates', as it has changed since we started the activity...");
AppPrefsProvider.Helper.update(this, app, app.getPrefs(this));
}
unregisterDownloaderReceiver();
}
private void unregisterDownloaderReceiver() {
if (localBroadcastManager == null) {
return;
}
localBroadcastManager.unregisterReceiver(downloadReceiver);
}
private void registerDownloaderReceiver() {
if (activeDownloadUrlString != null) { // if a download is active
String url = activeDownloadUrlString;
localBroadcastManager.registerReceiver(downloadReceiver,
DownloaderService.getIntentFilter(url));
}
}
private final BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Downloader.ACTION_STARTED:
if (headerFragment != null) {
headerFragment.startProgress();
}
break;
case Downloader.ACTION_PROGRESS:
if (headerFragment != null) {
headerFragment.updateProgress(intent.getIntExtra(Downloader.EXTRA_BYTES_READ, -1),
intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, -1));
}
break;
case Downloader.ACTION_COMPLETE:
// Starts the install process one the download is complete.
cleanUpFinishedDownload();
localBroadcastManager.registerReceiver(installReceiver,
Installer.getInstallIntentFilter(intent.getData()));
break;
case Downloader.ACTION_INTERRUPTED:
if (intent.hasExtra(Downloader.EXTRA_ERROR_MESSAGE)) {
String msg = intent.getStringExtra(Downloader.EXTRA_ERROR_MESSAGE)
+ " " + intent.getDataString();
Toast.makeText(context, R.string.download_error, Toast.LENGTH_SHORT).show();
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
} else { // user canceled
Toast.makeText(context, R.string.details_notinstalled, Toast.LENGTH_LONG).show();
}
cleanUpFinishedDownload();
break;
default:
throw new RuntimeException("intent action not handled!");
}
}
};
private final BroadcastReceiver installReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Installer.ACTION_INSTALL_STARTED:
headerFragment.startProgress(false);
headerFragment.showIndeterminateProgress(getString(R.string.installing));
break;
case Installer.ACTION_INSTALL_COMPLETE:
headerFragment.removeProgress();
localBroadcastManager.unregisterReceiver(this);
break;
case Installer.ACTION_INSTALL_INTERRUPTED:
headerFragment.removeProgress();
onAppChanged();
String errorMessage =
intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
if (!TextUtils.isEmpty(errorMessage)) {
Log.e(TAG, "install aborted with errorMessage: " + errorMessage);
String title = String.format(
getString(R.string.install_error_notify_title),
app.name);
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
alertBuilder.setTitle(title);
alertBuilder.setMessage(errorMessage);
alertBuilder.setNeutralButton(android.R.string.ok, null);
alertBuilder.create().show();
}
localBroadcastManager.unregisterReceiver(this);
break;
case Installer.ACTION_INSTALL_USER_INTERACTION:
PendingIntent installPendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
try {
installPendingIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "PI canceled", e);
}
break;
default:
throw new RuntimeException("intent action not handled!");
}
}
};
private final BroadcastReceiver uninstallReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Installer.ACTION_UNINSTALL_STARTED:
headerFragment.startProgress(false);
headerFragment.showIndeterminateProgress(getString(R.string.uninstalling));
break;
case Installer.ACTION_UNINSTALL_COMPLETE:
headerFragment.removeProgress();
onAppChanged();
localBroadcastManager.unregisterReceiver(this);
break;
case Installer.ACTION_UNINSTALL_INTERRUPTED:
headerFragment.removeProgress();
String errorMessage =
intent.getStringExtra(Installer.EXTRA_ERROR_MESSAGE);
if (!TextUtils.isEmpty(errorMessage)) {
Log.e(TAG, "uninstall aborted with errorMessage: " + errorMessage);
AlertDialog.Builder alertBuilder = new AlertDialog.Builder(AppDetails.this);
alertBuilder.setTitle(R.string.uninstall_error_notify_title);
alertBuilder.setMessage(errorMessage);
alertBuilder.setNeutralButton(android.R.string.ok, null);
alertBuilder.create().show();
}
localBroadcastManager.unregisterReceiver(this);
break;
case Installer.ACTION_UNINSTALL_USER_INTERACTION:
PendingIntent uninstallPendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
try {
uninstallPendingIntent.send();
} catch (PendingIntent.CanceledException e) {
Log.e(TAG, "PI canceled", e);
}
break;
default:
throw new RuntimeException("intent action not handled!");
}
}
};
private void onAppChanged() {
if (!reset(app.packageName)) {
this.finish();
return;
}
refreshApkList();
refreshHeader();
supportInvalidateOptionsMenu();
}
@Override
public Object onRetainCustomNonConfigurationInstance() {
return new ConfigurationChangeHelper(activeDownloadUrlString, app);
}
@Override
protected void onDestroy() {
unregisterDownloaderReceiver();
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;
calcActiveDownloadUrlString(packageName);
if (!TextUtils.isEmpty(packageName)) {
newApp = AppProvider.Helper.findHighestPriorityMetadata(getContentResolver(), packageName);
}
setApp(newApp);
return this.app != null;
}
private void calcActiveDownloadUrlString(String packageName) {
String urlString = getPreferences(MODE_PRIVATE).getString(packageName, null);
if (DownloaderService.isQueuedOrActive(urlString)) {
activeDownloadUrlString = urlString;
} else {
// this URL is no longer active, remove it
getPreferences(MODE_PRIVATE).edit().remove(packageName).apply();
}
}
/**
* 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;
startingPrefs = app.getPrefs(this).createClone();
}
private void refreshApkList() {
adapter.notifyDataSetChanged();
}
private void refreshHeader() {
if (headerFragment != null) {
headerFragment.updateViews();
}
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
menu.clear();
if (app == null) {
return true;
}
if (packageManager.getLaunchIntentForPackage(app.packageName) != null && app.canAndWantToUpdate(this)) {
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.getPrefs(context).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.getPrefs(context).ignoreThisUpdate >= app.suggestedVersionCode);
}
// 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(packageManager) == 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.suggestedVersionCode > 0) {
final Apk apkToInstall = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode);
install(apkToInstall);
}
return true;
case UNINSTALL:
uninstallApk();
return true;
case IGNOREALL:
app.getPrefs(this).ignoreAllUpdates ^= true;
item.setChecked(app.getPrefs(this).ignoreAllUpdates);
return true;
case IGNORETHIS:
if (app.getPrefs(this).ignoreThisUpdate >= app.suggestedVersionCode) {
app.getPrefs(this).ignoreThisUpdate = 0;
} else {
app.getPrefs(this).ignoreThisUpdate = app.suggestedVersionCode;
}
item.setChecked(app.getPrefs(this).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'.
private void install(final Apk apk) {
if (isFinishing()) {
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) {
initiateInstall(apk);
}
});
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;
}
initiateInstall(apk);
}
private void initiateInstall(Apk apk) {
Installer installer = InstallerFactory.create(this, apk);
Intent intent = installer.getPermissionScreen();
if (intent != null) {
// permission screen required
Utils.debugLog(TAG, "permission screen required");
startActivityForResult(intent, REQUEST_PERMISSION_DIALOG);
return;
}
startInstall(apk);
}
private void startInstall(Apk apk) {
activeDownloadUrlString = apk.getUrl();
registerDownloaderReceiver();
InstallManagerService.queue(this, app, apk);
}
/**
* Attempts to find the installed {@link Apk} from the database. If not found, will lookup the
* {@link InstalledAppProvider} to find the details of the installed app and use that to
* instantiate an {@link Apk} to be returned.
*
* Cases where an {@link Apk} will not be found in the database and for which we fall back to
* the {@link InstalledAppProvider} include:
* + System apps which are provided by a repository, but for which the version code bundled
* with the system is not included in the repository.
* + Regular apps from a repository, where the installed version is old enough that it is no
* longer available in the repository.
*
* @throws IllegalStateException If neither the {@link PackageManager} or the
* {@link InstalledAppProvider} can't find a reference to the installed apk.
*/
@NonNull
private Apk getInstalledApk() {
try {
PackageInfo pi = packageManager.getPackageInfo(app.packageName, 0);
Apk apk = ApkProvider.Helper.findApkFromAnyRepo(this, pi.packageName, pi.versionCode);
if (apk == null) {
InstalledApp installedApp = InstalledAppProvider.Helper.findByPackageName(context, pi.packageName);
if (installedApp == null) {
throw new IllegalStateException("No installed app found when trying to uninstall");
}
apk = new Apk(installedApp);
}
return apk;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
throw new IllegalStateException("Couldn't find app while installing");
}
}
/**
* Queue for uninstall based on the instance variable {@link #app}.
*/
private void uninstallApk() {
if (app.installedApk == null) {
// TODO ideally, app would be refreshed immediately after install, then this
// workaround would be unnecessary
app.installedApk = getInstalledApk();
}
Installer installer = InstallerFactory.create(this, app.installedApk);
Intent intent = installer.getUninstallScreen();
if (intent != null) {
// uninstall screen required
Utils.debugLog(TAG, "screen screen required");
startActivityForResult(intent, REQUEST_UNINSTALL_DIALOG);
return;
}
startUninstall();
}
private void startUninstall() {
localBroadcastManager.registerReceiver(uninstallReceiver,
Installer.getUninstallIntentFilter(app.packageName));
InstallerService.uninstall(context, app.installedApk);
}
private void launchApk(String packageName) {
Intent intent = packageManager.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
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case REQUEST_ENABLE_BLUETOOTH:
fdroidApp.sendViaBluetooth(this, resultCode, app.packageName);
break;
case REQUEST_PERMISSION_DIALOG:
if (resultCode == Activity.RESULT_OK) {
Uri uri = data.getData();
Apk apk = ApkProvider.Helper.findByUri(this, uri, Schema.ApkTable.Cols.ALL);
startInstall(apk);
}
break;
case REQUEST_UNINSTALL_DIALOG:
if (resultCode == Activity.RESULT_OK) {
startUninstall();
}
break;
}
}
private App getApp() {
return app;
}
private ApkListAdapter getApks() {
return adapter;
}
public static class AppDetailsSummaryFragment extends Fragment {
final Preferences prefs;
private AppDetails appDetails;
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 View permissionListView = llViewMorePermissions.findViewById(R.id.permission_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);
appDetails = (AppDetails) activity;
}
@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 onClickListener = new View.OnClickListener() {
public void onClick(View v) {
String url = null;
App app = appDetails.getApp();
switch (v.getId()) {
case R.id.website:
url = app.webURL;
break;
case R.id.email:
final String subject = Uri.encode(getString(R.string.app_details_subject, app.name));
url = "mailto:" + app.email + "?subject=" + subject;
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 = app.getBitcoinUri();
break;
case R.id.litecoin:
url = app.getLitecoinUri();
break;
case R.id.flattr:
url = app.getFlattrUri();
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);
if (Build.VERSION.SDK_INT > 10) {
// ellipsizing doesn't work properly here on 2.X
description.setEllipsize(TextUtils.TruncateAt.MARQUEE);
}
viewMorePermissions.setText(R.string.more);
}
viewAllDescription ^= true;
}
};
private void setupView(final View view) {
App app = appDetails.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);
if (Build.VERSION.SDK_INT > 10) {
// ellipsizing doesn't work properly here on 2.X
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(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
// Email button
tv = view.findViewById(R.id.email);
if (!TextUtils.isEmpty(app.email)) {
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
// Source button
tv = view.findViewById(R.id.source);
if (!TextUtils.isEmpty(app.sourceURL)) {
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
// Issues button
tv = view.findViewById(R.id.issues);
if (!TextUtils.isEmpty(app.trackerURL)) {
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
// Changelog button
tv = view.findViewById(R.id.changelog);
if (!TextUtils.isEmpty(app.changelogURL)) {
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
// Donate button
tv = view.findViewById(R.id.donate);
if (!TextUtils.isEmpty(app.donateURL)) {
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
// Bitcoin
tv = view.findViewById(R.id.bitcoin);
if (!TextUtils.isEmpty(app.bitcoinAddr)) {
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
// Litecoin
tv = view.findViewById(R.id.litecoin);
if (!TextUtils.isEmpty(app.litecoinAddr)) {
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
// Flattr
tv = view.findViewById(R.id.flattr);
if (!TextUtils.isEmpty(app.flattrID)) {
tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
Apk curApk = null;
for (int i = 0; i < appDetails.getApks().getCount(); i++) {
final Apk apk = appDetails.getApks().getItem(i);
if (apk.versionCode == app.suggestedVersionCode) {
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 (!appDetails.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() {
AppDiff appDiff = new AppDiff(appDetails.getPackageManager(), appDetails.getApks().getItem(0));
AppSecurityPermissions perms = new AppSecurityPermissions(appDetails, appDiff.pkgInfo);
final ViewGroup permList = (ViewGroup) llViewMorePermissions.findViewById(R.id.permission_list);
permList.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL));
}
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 = appDetails.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 AppDetails appDetails;
private Button btMain;
private ProgressBar progressBar;
private TextView progressSize;
private TextView progressPercent;
private ImageButton cancelButton;
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();
}
@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);
appDetails = (AppDetails) activity;
}
private void setupView(View view) {
App app = appDetails.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();
restoreProgressBarOnResume();
}
/**
* After resuming the fragment, decide whether or not we need to show the progress bar.
* Also, put an appropriate message depending on whether or not the download is active or
* just queued.
*
* NOTE: this can't be done in the `updateViews` method as it currently stands. The reason
* is because that method gets called all the time, for all sorts of reasons. The progress
* bar is updated with actual progress values in response to async broadcasts. If we always
* tried to force the progress bar in `updateViews`, it would override the values that were
* set by the async progress broadcasts.
*/
private void restoreProgressBarOnResume() {
if (appDetails.activeDownloadUrlString != null) {
// We don't actually know what the current progress is, so this will show an indeterminate
// progress bar until the first progress/complete event we receive.
if (DownloaderService.isQueuedOrActive(appDetails.activeDownloadUrlString)) {
showIndeterminateProgress(getString(R.string.download_pending));
} else {
showIndeterminateProgress("");
}
}
}
/**
* Displays empty, indeterminate progress bar and related views.
*/
public void startProgress() {
startProgress(true);
}
public void startProgress(boolean allowCancel) {
cancelButton.setVisibility(allowCancel ? View.VISIBLE : View.GONE);
if (isAdded()) {
showIndeterminateProgress(getString(R.string.download_pending));
updateViews();
}
}
private void showIndeterminateProgress(String message) {
setProgressVisible(true);
progressBar.setIndeterminate(true);
progressSize.setText(message);
progressPercent.setText("");
}
/**
* 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);
}
/**
* Removes progress bar and related views, invokes {@link #updateViews()}.
*/
public void removeProgress() {
setProgressVisible(false);
cancelButton.setVisibility(View.GONE);
updateViews();
}
/**
* Cancels download and hides progress bar.
*/
@Override
public void onClick(View view) {
AppDetails appDetails = (AppDetails) getActivity();
if (appDetails == null || appDetails.activeDownloadUrlString == null) {
return;
}
InstallManagerService.cancel(getContext(), appDetails.activeDownloadUrlString);
}
public void updateViews() {
updateViews(getView());
}
public void updateViews(View view) {
if (view == null) {
Log.e(TAG, "AppDetailsHeaderFragment.updateViews(): view == null. Oops.");
return;
}
App app = appDetails.getApp();
TextView statusView = (TextView) view.findViewById(R.id.status);
btMain.setVisibility(View.VISIBLE);
if (appDetails.activeDownloadUrlString != null) {
btMain.setText(R.string.downloading);
btMain.setEnabled(false);
} else if (!app.isInstalled() && app.suggestedVersionCode > 0 &&
appDetails.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(appDetails);
// Set Install button and hide second button
btMain.setText(R.string.menu_install);
btMain.setOnClickListener(onClickListener);
btMain.setEnabled(true);
} else if (app.isInstalled()) {
// If App is installed
installed = true;
statusView.setText(getString(R.string.details_installed, app.installedVersionName));
NfcHelper.setAndroidBeam(appDetails, app.packageName);
if (app.canAndWantToUpdate(appDetails)) {
updateWanted = true;
btMain.setText(R.string.menu_upgrade);
} else {
updateWanted = false;
if (appDetails.packageManager.getLaunchIntentForPackage(app.packageName) != null) {
btMain.setText(R.string.menu_launch);
} else {
btMain.setText(R.string.menu_uninstall);
}
}
btMain.setOnClickListener(onClickListener);
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 (!appDetails.getApks().isEmpty()) {
currentVersion.setText(appDetails.getApks().getItem(0).versionName + " (" + app.license + ")");
} else {
currentVersion.setVisibility(View.GONE);
btMain.setVisibility(View.GONE);
}
}
private final View.OnClickListener onClickListener = new View.OnClickListener() {
public void onClick(View v) {
App app = appDetails.getApp();
AppDetails activity = (AppDetails) getActivity();
if (updateWanted && app.suggestedVersionCode > 0) {
Apk apkToInstall = ApkProvider.Helper.findApkFromAnyRepo(activity, app.packageName, app.suggestedVersionCode);
activity.install(apkToInstall);
return;
}
if (installed) {
// If installed
if (activity.packageManager.getLaunchIntentForPackage(app.packageName) != null) {
// If "launchable", launch
activity.launchApk(app.packageName);
} else {
activity.uninstallApk();
}
} else if (app.suggestedVersionCode > 0) {
// If not installed, install
btMain.setEnabled(false);
btMain.setText(R.string.system_install_installing);
final Apk apkToInstall = ApkProvider.Helper.findApkFromAnyRepo(activity, app.packageName, app.suggestedVersionCode);
activity.install(apkToInstall);
}
}
};
}
public static class AppDetailsListFragment extends ListFragment {
private static final String SUMMARY_TAG = "summary";
private AppDetails appDetails;
private AppDetailsSummaryFragment summaryFragment;
private FrameLayout headerView;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
appDetails = (AppDetails) activity;
}
@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(appDetails.getApks());
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
App app = appDetails.getApp();
final Apk apk = appDetails.getApks().getItem(position - l.getHeaderViewsCount());
if (app.installedVersionCode == apk.versionCode) {
appDetails.uninstallApk();
} else if (app.installedVersionCode > apk.versionCode) {
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) {
appDetails.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 {
appDetails.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);
}
}
}