package org.fdroid.fdroid;
import android.app.Activity;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
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.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.CoordinatorLayout;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ImageView;
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.AppPrefsProvider;
import org.fdroid.fdroid.data.AppProvider;
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.views.AppDetailsRecyclerViewAdapter;
import org.fdroid.fdroid.views.ShareChooserDialog;
public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog.ShareChooserDialogListener, AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks {
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
private static final String TAG = "AppDetails2";
private static final int REQUEST_ENABLE_BLUETOOTH = 2;
private static final int REQUEST_PERMISSION_DIALOG = 3;
private static final int REQUEST_UNINSTALL_DIALOG = 4;
private FDroidApp fdroidApp;
private App app;
private RecyclerView recyclerView;
private AppDetailsRecyclerViewAdapter adapter;
private LocalBroadcastManager localBroadcastManager;
private String activeDownloadUrlString;
@Override
protected void onCreate(Bundle savedInstanceState) {
fdroidApp = (FDroidApp) getApplication();
//fdroidApp.applyTheme(this);
super.onCreate(savedInstanceState);
setContentView(R.layout.app_details2);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
toolbar.setTitle(""); // Nice and clean toolbar
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (!reset(getPackageNameFromIntent(getIntent()))) {
finish();
return;
}
localBroadcastManager = LocalBroadcastManager.getInstance(this);
recyclerView = (RecyclerView) findViewById(R.id.rvDetails);
adapter = new AppDetailsRecyclerViewAdapter(this, app, this);
LinearLayoutManager lm = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
lm.setStackFromEnd(false);
recyclerView.setLayoutManager(lm);
recyclerView.setAdapter(adapter);
// Load the feature graphic, if present
if (!TextUtils.isEmpty(app.iconUrlLarge)) {
ImageView ivFeatureGraphic = (ImageView) findViewById(R.id.feature_graphic);
DisplayImageOptions displayImageOptions = new DisplayImageOptions.Builder()
.cacheInMemory(false)
.cacheOnDisk(true)
.imageScaleType(ImageScaleType.NONE)
.bitmapConfig(Bitmap.Config.RGB_565)
.build();
ImageLoader.getInstance().displayImage(app.iconUrlLarge, ivFeatureGraphic, displayImageOptions);
}
}
private String getPackageNameFromIntent(Intent intent) {
if (!intent.hasExtra(AppDetails.EXTRA_APPID)) {
Log.e(TAG, "No package name found in the intent!");
return null;
}
return intent.getStringExtra(AppDetails.EXTRA_APPID);
}
/**
* 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;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
boolean ret = super.onCreateOptionsMenu(menu);
if (ret) {
getMenuInflater().inflate(R.menu.details2, menu);
}
return ret;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu);
if (app == null) {
return true;
}
MenuItem itemIgnoreAll = menu.findItem(R.id.action_ignore_all);
if (itemIgnoreAll != null) {
itemIgnoreAll.setChecked(app.getPrefs(this).ignoreAllUpdates);
}
MenuItem itemIgnoreThis = menu.findItem(R.id.action_ignore_this);
if (itemIgnoreThis != null) {
itemIgnoreThis.setVisible(app.hasUpdates());
itemIgnoreThis.setChecked(app.getPrefs(this).ignoreThisUpdate >= app.suggestedVersionCode);
}
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.action_share) {
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);
boolean showNearbyItem = app.isInstalled() && fdroidApp.bluetoothAdapter != null;
ShareChooserDialog.createChooser((CoordinatorLayout) findViewById(R.id.rootCoordinator), this, this, shareIntent, showNearbyItem);
return true;
} else if (item.getItemId() == R.id.action_ignore_all) {
app.getPrefs(this).ignoreAllUpdates ^= true;
item.setChecked(app.getPrefs(this).ignoreAllUpdates);
AppPrefsProvider.Helper.update(this, app, app.getPrefs(this));
return true;
} else if (item.getItemId() == R.id.action_ignore_this) {
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);
AppPrefsProvider.Helper.update(this, app, app.getPrefs(this));
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onNearby() {
// 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
}
@Override
public void onResolvedShareIntent(Intent shareIntent) {
startActivity(shareIntent);
}
@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;
}
}
// Install the version of this app denoted by 'app.curApk'.
@Override
public void installApk(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);
}
private void startUninstall() {
registerUninstallReceiver();
InstallerService.uninstall(this, app.installedApk);
}
private void registerUninstallReceiver() {
localBroadcastManager.registerReceiver(uninstallReceiver,
Installer.getUninstallIntentFilter(app.packageName));
}
private void unregisterUninstallReceiver() {
localBroadcastManager.unregisterReceiver(uninstallReceiver);
}
private void registerDownloaderReceiver() {
if (activeDownloadUrlString != null) { // if a download is active
String url = activeDownloadUrlString;
localBroadcastManager.registerReceiver(downloadReceiver,
DownloaderService.getIntentFilter(url));
}
}
private void unregisterDownloaderReceiver() {
localBroadcastManager.unregisterReceiver(downloadReceiver);
}
private void unregisterInstallReceiver() {
localBroadcastManager.unregisterReceiver(installReceiver);
}
private final BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Downloader.ACTION_STARTED:
adapter.setProgress(-1, -1, R.string.download_pending);
break;
case Downloader.ACTION_PROGRESS:
adapter.setProgress(intent.getIntExtra(Downloader.EXTRA_BYTES_READ, -1),
intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, -1), 0);
break;
case Downloader.ACTION_COMPLETE:
// Starts the install process once 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:
adapter.setProgress(-1, -1, R.string.installing);
break;
case Installer.ACTION_INSTALL_COMPLETE:
adapter.clearProgress();
unregisterInstallReceiver();
onAppChanged();
break;
case Installer.ACTION_INSTALL_INTERRUPTED:
adapter.clearProgress();
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(AppDetails2.this);
alertBuilder.setTitle(title);
alertBuilder.setMessage(errorMessage);
alertBuilder.setNeutralButton(android.R.string.ok, null);
alertBuilder.create().show();
}
unregisterInstallReceiver();
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:
adapter.setProgress(-1, -1, R.string.uninstalling);
break;
case Installer.ACTION_UNINSTALL_COMPLETE:
adapter.clearProgress();
onAppChanged();
unregisterUninstallReceiver();
break;
case Installer.ACTION_UNINSTALL_INTERRUPTED:
adapter.clearProgress();
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(AppDetails2.this);
alertBuilder.setTitle(R.string.uninstall_error_notify_title);
alertBuilder.setMessage(errorMessage);
alertBuilder.setNeutralButton(android.R.string.ok, null);
alertBuilder.create().show();
}
unregisterUninstallReceiver();
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!");
}
}
};
/**
* 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();
}
}
/**
* Remove progress listener, suppress progress bar, set downloadHandler to null.
*/
private void cleanUpFinishedDownload() {
activeDownloadUrlString = null;
adapter.clearProgress();
unregisterDownloaderReceiver();
}
private void onAppChanged() {
recyclerView.post(new Runnable() {
@Override
public void run() {
if (!reset(app.packageName)) {
AppDetails2.this.finish();
return;
}
AppDetailsRecyclerViewAdapter adapter = (AppDetailsRecyclerViewAdapter) recyclerView.getAdapter();
adapter.updateItems(app);
supportInvalidateOptionsMenu();
}
});
}
@Override
public boolean isAppDownloading() {
return !TextUtils.isEmpty(activeDownloadUrlString);
}
@Override
public void enableAndroidBeam() {
NfcHelper.setAndroidBeam(this, app.packageName);
}
@Override
public void disableAndroidBeam() {
NfcHelper.disableAndroidBeam(this);
}
@Override
public void openUrl(String url) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
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 void installCancel() {
if (!TextUtils.isEmpty(activeDownloadUrlString)) {
InstallManagerService.cancel(this, activeDownloadUrlString);
}
}
@Override
public void launchApk() {
Intent intent = getPackageManager().getLaunchIntentForPackage(app.packageName);
startActivity(intent);
}
@Override
public void installApk() {
Apk apkToInstall = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode);
installApk(apkToInstall);
}
@Override
public void upgradeApk() {
Apk apkToInstall = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode);
installApk(apkToInstall);
}
@Override
public void uninstallApk() {
Apk apk = app.installedApk;
if (apk == null) {
// TODO ideally, app would be refreshed immediately after install, then this
// workaround would be unnecessary
try {
PackageInfo pi = getPackageManager().getPackageInfo(app.packageName, 0);
apk = ApkProvider.Helper.findApkFromAnyRepo(this, pi.packageName, pi.versionCode);
app.installedApk = apk;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return; // not installed
}
}
Installer installer = InstallerFactory.create(this, apk);
Intent intent = installer.getUninstallScreen();
if (intent != null) {
// uninstall screen required
Utils.debugLog(TAG, "screen screen required");
startActivityForResult(intent, REQUEST_UNINSTALL_DIALOG);
return;
}
startUninstall();
}
}