diff --git a/app/build.gradle b/app/build.gradle
index 01ddd57ed..1af92e952 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -21,6 +21,9 @@ dependencies {
compile 'com.android.support:support-v4:24.2.1'
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.android.support:support-annotations:24.2.1'
+ compile 'com.android.support:design:24.2.1'
+ compile 'com.android.support:cardview-v7:24.2.1'
+ compile "com.android.support:recyclerview-v7:24.2.1"
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
compile 'com.google.zxing:core:3.2.1'
@@ -41,6 +44,9 @@ dependencies {
testCompile "org.robolectric:robolectric:3.1.2"
+ // As per https://github.com/robolectric/robolectric/issues/1932#issuecomment-219796474
+ testCompile 'org.khronos:opengl-api:gl1.1-android-2.1_r1'
+
testCompile "org.mockito:mockito-core:1.10.19"
androidTestCompile 'com.android.support:support-annotations:24.2.1'
@@ -97,6 +103,9 @@ if (!hasProperty('sourceDeps')) {
'com.android.support:support-media-compat:fa29a23eadd685631584b2c0c624a36e3bb79a33e257b00304501ad682fa2be3',
'com.android.support:support-v4:cac2956f5c4bb363cc0ba824ac16ea2a687d1c305d434416a34772a5f9375ed7',
'com.android.support:support-vector-drawable:6ee37a7f7b93c1df1294e6f6f97df3724ac989fcda0549faf677001085330548',
+ 'com.android.support:design:89842bb1243507fe3079066ea4ea58795effe69cdf9a819e05274d21760adfc2',
+ 'com.android.support:cardview-v7:2303b351686d1db060b5fcf1a9c709c79b4a54a85bfda0fb3c4849e244606ee1',
+ 'com.android.support:recyclerview-v7:9077766a1a0f4e89528fbf9dcdf6d5880a8686f0266fa852d58d803beeef18fa',
'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
'com.madgag.spongycastle:core:9b6b7ac856b91bcda2ede694eccd26cefb0bf0b09b89f13cda05b5da5ff68c6b',
'com.madgag.spongycastle:pkix:6aba9b2210907a3d46dd3dcac782bb3424185290468d102d5207ebdc9796a905',
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6d927ae32..08c034216 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -404,6 +404,17 @@
android:value=".FDroid" />
+
+
+
{
- private final LayoutInflater mInflater = (LayoutInflater) context.getSystemService(
+ private final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
ApkListAdapter(Context context, App app) {
@@ -204,7 +204,7 @@ public class AppDetails extends AppCompatActivity {
ViewHolder holder;
if (convertView == null) {
- convertView = mInflater.inflate(R.layout.apklistitem, parent, false);
+ convertView = inflater.inflate(R.layout.apklistitem, parent, false);
holder = new ViewHolder();
holder.version = (TextView) convertView.findViewById(R.id.version);
@@ -1157,7 +1157,7 @@ public class AppDetails extends AppCompatActivity {
}
};
- private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
+ private final View.OnClickListener onClickListener = new View.OnClickListener() {
public void onClick(View v) {
String url = null;
App app = appDetails.getApp();
@@ -1182,13 +1182,13 @@ public class AppDetails extends AppCompatActivity {
url = app.donateURL;
break;
case R.id.bitcoin:
- url = "bitcoin:" + app.bitcoinAddr;
+ url = app.getBitcoinUri();
break;
case R.id.litecoin:
- url = "litecoin:" + app.litecoinAddr;
+ url = app.getLitecoinUri();
break;
case R.id.flattr:
- url = "https://flattr.com/thing/" + app.flattrID;
+ url = app.getFlattrUri();
break;
}
if (url != null) {
@@ -1268,7 +1268,7 @@ public class AppDetails extends AppCompatActivity {
// Website button
View tv = view.findViewById(R.id.website);
if (!TextUtils.isEmpty(app.webURL)) {
- tv.setOnClickListener(mOnClickListener);
+ tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@@ -1276,7 +1276,7 @@ public class AppDetails extends AppCompatActivity {
// Email button
tv = view.findViewById(R.id.email);
if (!TextUtils.isEmpty(app.email)) {
- tv.setOnClickListener(mOnClickListener);
+ tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@@ -1284,7 +1284,7 @@ public class AppDetails extends AppCompatActivity {
// Source button
tv = view.findViewById(R.id.source);
if (!TextUtils.isEmpty(app.sourceURL)) {
- tv.setOnClickListener(mOnClickListener);
+ tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@@ -1292,7 +1292,7 @@ public class AppDetails extends AppCompatActivity {
// Issues button
tv = view.findViewById(R.id.issues);
if (!TextUtils.isEmpty(app.trackerURL)) {
- tv.setOnClickListener(mOnClickListener);
+ tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@@ -1300,7 +1300,7 @@ public class AppDetails extends AppCompatActivity {
// Changelog button
tv = view.findViewById(R.id.changelog);
if (!TextUtils.isEmpty(app.changelogURL)) {
- tv.setOnClickListener(mOnClickListener);
+ tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@@ -1308,7 +1308,7 @@ public class AppDetails extends AppCompatActivity {
// Donate button
tv = view.findViewById(R.id.donate);
if (!TextUtils.isEmpty(app.donateURL)) {
- tv.setOnClickListener(mOnClickListener);
+ tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@@ -1316,7 +1316,7 @@ public class AppDetails extends AppCompatActivity {
// Bitcoin
tv = view.findViewById(R.id.bitcoin);
if (!TextUtils.isEmpty(app.bitcoinAddr)) {
- tv.setOnClickListener(mOnClickListener);
+ tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@@ -1324,7 +1324,7 @@ public class AppDetails extends AppCompatActivity {
// Litecoin
tv = view.findViewById(R.id.litecoin);
if (!TextUtils.isEmpty(app.litecoinAddr)) {
- tv.setOnClickListener(mOnClickListener);
+ tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@@ -1332,7 +1332,7 @@ public class AppDetails extends AppCompatActivity {
// Flattr
tv = view.findViewById(R.id.flattr);
if (!TextUtils.isEmpty(app.flattrID)) {
- tv.setOnClickListener(mOnClickListener);
+ tv.setOnClickListener(onClickListener);
} else {
tv.setVisibility(View.GONE);
}
@@ -1620,7 +1620,7 @@ public class AppDetails extends AppCompatActivity {
NfcHelper.disableAndroidBeam(appDetails);
// Set Install button and hide second button
btMain.setText(R.string.menu_install);
- btMain.setOnClickListener(mOnClickListener);
+ btMain.setOnClickListener(onClickListener);
btMain.setEnabled(true);
} else if (app.isInstalled()) {
// If App is installed
@@ -1638,7 +1638,7 @@ public class AppDetails extends AppCompatActivity {
btMain.setText(R.string.menu_uninstall);
}
}
- btMain.setOnClickListener(mOnClickListener);
+ btMain.setOnClickListener(onClickListener);
btMain.setEnabled(true);
}
TextView author = (TextView) view.findViewById(R.id.author);
@@ -1656,7 +1656,7 @@ public class AppDetails extends AppCompatActivity {
}
- private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
+ private final View.OnClickListener onClickListener = new View.OnClickListener() {
public void onClick(View v) {
App app = appDetails.getApp();
AppDetails activity = (AppDetails) getActivity();
diff --git a/app/src/main/java/org/fdroid/fdroid/AppDetails2.java b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java
new file mode 100644
index 000000000..52e06311f
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/AppDetails2.java
@@ -0,0 +1,575 @@
+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();
+ }
+}
diff --git a/app/src/main/java/org/fdroid/fdroid/Utils.java b/app/src/main/java/org/fdroid/fdroid/Utils.java
index eca09c6bd..08a39be78 100644
--- a/app/src/main/java/org/fdroid/fdroid/Utils.java
+++ b/app/src/main/java/org/fdroid/fdroid/Utils.java
@@ -20,6 +20,7 @@ package org.fdroid.fdroid;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
@@ -31,6 +32,7 @@ import android.text.Html;
import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
+import android.util.TypedValue;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.assist.ImageScaleType;
@@ -596,4 +598,8 @@ public final class Utils {
return data;
}
+ public static int dpToPx(int dp, Context ctx) {
+ Resources r = ctx.getResources();
+ return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics());
+ }
}
diff --git a/app/src/main/java/org/fdroid/fdroid/compat/PRNGFixes.java b/app/src/main/java/org/fdroid/fdroid/compat/PRNGFixes.java
index 667d1fd70..7dc71f525 100644
--- a/app/src/main/java/org/fdroid/fdroid/compat/PRNGFixes.java
+++ b/app/src/main/java/org/fdroid/fdroid/compat/PRNGFixes.java
@@ -202,7 +202,7 @@ public final class PRNGFixes {
* each instance needs to seed itself if the client does not explicitly
* seed it.
*/
- private boolean mSeeded;
+ private boolean seeded;
@Override
protected void engineSetSeed(byte[] bytes) {
@@ -218,13 +218,13 @@ public final class PRNGFixes {
// Log and ignore.
Log.w(TAG, "Failed to mix seed into " + URANDOM_FILE);
} finally {
- mSeeded = true;
+ seeded = true;
}
}
@Override
protected void engineNextBytes(byte[] bytes) {
- if (!mSeeded) {
+ if (!seeded) {
// Mix in the device- and invocation-specific seed.
engineSetSeed(generateSeed());
}
diff --git a/app/src/main/java/org/fdroid/fdroid/data/App.java b/app/src/main/java/org/fdroid/fdroid/data/App.java
index 671fca3cd..60e5cfd44 100644
--- a/app/src/main/java/org/fdroid/fdroid/data/App.java
+++ b/app/src/main/java/org/fdroid/fdroid/data/App.java
@@ -12,6 +12,7 @@ import android.database.Cursor;
import android.os.Environment;
import android.os.Parcel;
import android.os.Parcelable;
+import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
@@ -552,6 +553,21 @@ public class App extends ValueObject implements Comparable, Parcelable {
return new AppFilter().filter(this);
}
+ @Nullable
+ public String getBitcoinUri() {
+ return TextUtils.isEmpty(bitcoinAddr) ? null : "bitcoin:" + bitcoinAddr;
+ }
+
+ @Nullable
+ public String getLitecoinUri() {
+ return TextUtils.isEmpty(litecoinAddr) ? null : "litecoin:" + litecoinAddr;
+ }
+
+ @Nullable
+ public String getFlattrUri() {
+ return TextUtils.isEmpty(flattrID) ? null : "https://flattr.com/thing/" + flattrID;
+ }
+
public String getSuggestedVersionName() {
return suggestedVersionName;
}
diff --git a/app/src/main/java/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java b/app/src/main/java/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java
index fbee8f591..587cb91bb 100644
--- a/app/src/main/java/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java
+++ b/app/src/main/java/org/fdroid/fdroid/localrepo/peers/BonjourFinder.java
@@ -46,7 +46,7 @@ final class BonjourFinder extends PeerFinder implements ServiceListener {
private JmDNS jmdns;
private WifiManager wifiManager;
- private WifiManager.MulticastLock mMulticastLock;
+ private WifiManager.MulticastLock multicastLock;
private BonjourFinder(Context context, Subscriber super Peer> subscriber) {
super(context, subscriber);
@@ -58,8 +58,8 @@ final class BonjourFinder extends PeerFinder implements ServiceListener {
if (wifiManager == null) {
wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
- mMulticastLock = wifiManager.createMulticastLock(context.getPackageName());
- mMulticastLock.setReferenceCounted(false);
+ multicastLock = wifiManager.createMulticastLock(context.getPackageName());
+ multicastLock.setReferenceCounted(false);
}
if (isScanning) {
@@ -68,7 +68,7 @@ final class BonjourFinder extends PeerFinder implements ServiceListener {
}
isScanning = true;
- mMulticastLock.acquire();
+ multicastLock.acquire();
try {
Utils.debugLog(TAG, "Searching for Bonjour (mDNS) clients...");
@@ -146,8 +146,8 @@ final class BonjourFinder extends PeerFinder implements ServiceListener {
private void cancel() {
Utils.debugLog(TAG, "Cancelling BonjourFinder, releasing multicast lock, removing jmdns service listeners");
- if (mMulticastLock != null) {
- mMulticastLock.release();
+ if (multicastLock != null) {
+ multicastLock.release();
}
isScanning = false;
diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java b/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java
index 1354a789c..c6be97c07 100644
--- a/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java
+++ b/app/src/main/java/org/fdroid/fdroid/privileged/install/InstallExtensionDialogActivity.java
@@ -129,7 +129,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
* 1. Check for root access
*/
private final AsyncTask checkRootTask = new AsyncTask() {
- ProgressDialog mProgressDialog;
+ ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
@@ -139,11 +139,11 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
ContextThemeWrapper theme = new ContextThemeWrapper(InstallExtensionDialogActivity.this,
FDroidApp.getCurThemeResId());
- mProgressDialog = new ProgressDialog(theme);
- mProgressDialog.setMessage(getString(R.string.requesting_root_access_body));
- mProgressDialog.setIndeterminate(true);
- mProgressDialog.setCancelable(false);
- mProgressDialog.show();
+ progressDialog = new ProgressDialog(theme);
+ progressDialog.setMessage(getString(R.string.requesting_root_access_body));
+ progressDialog.setIndeterminate(true);
+ progressDialog.setCancelable(false);
+ progressDialog.show();
}
@Override
@@ -155,7 +155,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
protected void onPostExecute(Boolean rootGranted) {
super.onPostExecute(rootGranted);
- mProgressDialog.dismiss();
+ progressDialog.dismiss();
if (rootGranted) {
// root access granted
@@ -193,7 +193,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
* 2. Install into system
*/
private final AsyncTask installTask = new AsyncTask() {
- ProgressDialog mProgressDialog;
+ ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
@@ -203,11 +203,11 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
ContextThemeWrapper theme = new ContextThemeWrapper(InstallExtensionDialogActivity.this,
FDroidApp.getCurThemeResId());
- mProgressDialog = new ProgressDialog(theme);
- mProgressDialog.setMessage(InstallExtension.create(getApplicationContext()).getInstallingString());
- mProgressDialog.setIndeterminate(true);
- mProgressDialog.setCancelable(false);
- mProgressDialog.show();
+ progressDialog = new ProgressDialog(theme);
+ progressDialog.setMessage(InstallExtension.create(getApplicationContext()).getInstallingString());
+ progressDialog.setIndeterminate(true);
+ progressDialog.setCancelable(false);
+ progressDialog.show();
}
@Override
@@ -298,7 +298,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
}
private final AsyncTask uninstallTask = new AsyncTask() {
- ProgressDialog mProgressDialog;
+ ProgressDialog progressDialog;
@Override
protected void onPreExecute() {
@@ -308,11 +308,11 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
ContextThemeWrapper theme = new ContextThemeWrapper(InstallExtensionDialogActivity.this,
FDroidApp.getCurThemeResId());
- mProgressDialog = new ProgressDialog(theme);
- mProgressDialog.setMessage(getString(R.string.system_install_uninstalling));
- mProgressDialog.setIndeterminate(true);
- mProgressDialog.setCancelable(false);
- mProgressDialog.show();
+ progressDialog = new ProgressDialog(theme);
+ progressDialog.setMessage(getString(R.string.system_install_uninstalling));
+ progressDialog.setIndeterminate(true);
+ progressDialog.setCancelable(false);
+ progressDialog.show();
}
@Override
@@ -325,7 +325,7 @@ public class InstallExtensionDialogActivity extends FragmentActivity {
protected void onPostExecute(Void unused) {
super.onPostExecute(unused);
- mProgressDialog.dismiss();
+ progressDialog.dismiss();
// app is uninstalled but still display, kill it!
System.exit(0);
diff --git a/app/src/main/java/org/fdroid/fdroid/privileged/views/AppSecurityPermissions.java b/app/src/main/java/org/fdroid/fdroid/privileged/views/AppSecurityPermissions.java
index e4b2b1c4a..74176e9f4 100644
--- a/app/src/main/java/org/fdroid/fdroid/privileged/views/AppSecurityPermissions.java
+++ b/app/src/main/java/org/fdroid/fdroid/privileged/views/AppSecurityPermissions.java
@@ -90,7 +90,7 @@ public class AppSecurityPermissions {
// PermissionGroupInfo implements Parcelable but its Parcel constructor is private and thus cannot be extended.
@SuppressLint("ParcelCreator")
static class MyPermissionGroupInfo extends PermissionGroupInfo {
- CharSequence mLabel;
+ CharSequence label;
final List newPermissions = new ArrayList<>();
final List allPermissions = new ArrayList<>();
@@ -185,7 +185,7 @@ public class AppSecurityPermissions {
}
PackageManager pm = getContext().getPackageManager();
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
- builder.setTitle(group.mLabel);
+ builder.setTitle(group.label);
if (perm.descriptionRes != 0) {
builder.setMessage(perm.loadDescription(pm));
} else {
@@ -431,22 +431,22 @@ public class AppSecurityPermissions {
private static class PermissionGroupInfoComparator implements Comparator {
- private final Collator sCollator = Collator.getInstance();
+ private final Collator collator = Collator.getInstance();
public final int compare(MyPermissionGroupInfo a, MyPermissionGroupInfo b) {
- return sCollator.compare(a.mLabel, b.mLabel);
+ return collator.compare(a.label, b.label);
}
}
private static class PermissionInfoComparator implements Comparator {
- private final Collator sCollator = Collator.getInstance();
+ private final Collator collator = Collator.getInstance();
PermissionInfoComparator() {
}
public final int compare(MyPermissionInfo a, MyPermissionInfo b) {
- return sCollator.compare(a.label, b.label);
+ return collator.compare(a.label, b.label);
}
}
@@ -482,13 +482,13 @@ public class AppSecurityPermissions {
for (MyPermissionGroupInfo pgrp : permGroups.values()) {
if (pgrp.labelRes != 0 || pgrp.nonLocalizedLabel != null) {
- pgrp.mLabel = pgrp.loadLabel(pm);
+ pgrp.label = pgrp.loadLabel(pm);
} else {
try {
ApplicationInfo app = pm.getApplicationInfo(pgrp.packageName, 0);
- pgrp.mLabel = app.loadLabel(pm);
+ pgrp.label = app.loadLabel(pm);
} catch (NameNotFoundException e) {
- pgrp.mLabel = pgrp.loadLabel(pm);
+ pgrp.label = pgrp.loadLabel(pm);
}
}
permGroupsList.add(pgrp);
diff --git a/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java
new file mode 100644
index 000000000..a14b8602d
--- /dev/null
+++ b/app/src/main/java/org/fdroid/fdroid/views/AppDetailsRecyclerViewAdapter.java
@@ -0,0 +1,881 @@
+package org.fdroid.fdroid.views;
+
+import android.annotation.SuppressLint;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.v4.view.ViewCompat;
+import android.support.v4.widget.TextViewCompat;
+import android.support.v7.text.AllCapsTransformationMethod;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.text.Html;
+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.URLSpan;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+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.Preferences;
+import org.fdroid.fdroid.R;
+import org.fdroid.fdroid.Utils;
+import org.fdroid.fdroid.data.Apk;
+import org.fdroid.fdroid.data.ApkProvider;
+import org.fdroid.fdroid.data.App;
+import org.fdroid.fdroid.data.InstalledAppProvider;
+import org.fdroid.fdroid.data.RepoProvider;
+import org.fdroid.fdroid.privileged.views.AppDiff;
+import org.fdroid.fdroid.privileged.views.AppSecurityPermissions;
+
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+public class AppDetailsRecyclerViewAdapter
+ extends RecyclerView.Adapter {
+
+ public interface AppDetailsRecyclerViewAdapterCallbacks {
+
+ boolean isAppDownloading();
+
+ void enableAndroidBeam();
+
+ void disableAndroidBeam();
+
+ void openUrl(String url);
+
+ void installApk();
+
+ void installApk(Apk apk);
+
+ void upgradeApk();
+
+ void uninstallApk();
+
+ void installCancel();
+
+ void launchApk();
+
+ }
+
+ private static final int VIEWTYPE_HEADER = 0;
+ private static final int VIEWTYPE_SCREENSHOTS = 1;
+ private static final int VIEWTYPE_WHATS_NEW = 2;
+ private static final int VIEWTYPE_DONATE = 3;
+ private static final int VIEWTYPE_LINKS = 4;
+ private static final int VIEWTYPE_PERMISSIONS = 5;
+ private static final int VIEWTYPE_VERSIONS = 6;
+ private static final int VIEWTYPE_VERSION = 7;
+
+ private final Context context;
+ @NonNull
+ private App app;
+ private final AppDetailsRecyclerViewAdapterCallbacks callbacks;
+ private RecyclerView recyclerView;
+ private ArrayList