Add apps with known vulnerabilities to updates tab.

Currently only supports "Uninstall", but will soon also support "Upgrade
to a newer version".
This commit is contained in:
Peter Serwylo 2017-07-05 17:21:09 +10:00
parent 9b20142fd9
commit 7424220c02
8 changed files with 335 additions and 73 deletions

View File

@ -388,55 +388,59 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
return;
}
// When the button says "Run", then launch the app.
if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.Installed) {
Intent intent = activity.getPackageManager().getLaunchIntentForPackage(currentApp.packageName);
if (intent != null) {
activity.startActivity(intent);
// Once it is explicitly launched by the user, then we can pretty much forget about
// any sort of notification that the app was successfully installed. It should be
// apparent to the user because they just launched it.
AppUpdateStatusManager.getInstance(activity).removeApk(currentStatus.getUniqueKey());
}
return;
}
if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) {
Uri apkDownloadUri = Uri.parse(currentStatus.apk.getUrl());
File apkFilePath = ApkCache.getApkDownloadPath(activity, apkDownloadUri);
Utils.debugLog(TAG, "skip download, we have already downloaded " + currentStatus.apk.getUrl() +
" to " + apkFilePath);
// TODO: This seems like a bit of a hack. Is there a better way to do this by changing
// the Installer API so that we can ask it to install without having to get it to fire
// off an intent which we then listen for and action?
final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(activity);
final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
broadcastManager.unregisterReceiver(this);
if (Installer.ACTION_INSTALL_USER_INTERACTION.equals(intent.getAction())) {
PendingIntent pendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
try {
pendingIntent.send();
} catch (PendingIntent.CanceledException ignored) { }
}
}
};
broadcastManager.registerReceiver(receiver, Installer.getInstallIntentFilter(apkDownloadUri));
Installer installer = InstallerFactory.create(activity, currentStatus.apk);
installer.installPackage(Uri.parse(apkFilePath.toURI().toString()), apkDownloadUri);
} else {
final Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, currentApp);
InstallManagerService.queue(activity, currentApp, suggestedApk);
}
onActionButtonPressed(currentApp);
}
};
protected void onActionButtonPressed(@NonNull App app) {
// When the button says "Run", then launch the app.
if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.Installed) {
Intent intent = activity.getPackageManager().getLaunchIntentForPackage(app.packageName);
if (intent != null) {
activity.startActivity(intent);
// Once it is explicitly launched by the user, then we can pretty much forget about
// any sort of notification that the app was successfully installed. It should be
// apparent to the user because they just launched it.
AppUpdateStatusManager.getInstance(activity).removeApk(currentStatus.getUniqueKey());
}
return;
}
if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) {
Uri apkDownloadUri = Uri.parse(currentStatus.apk.getUrl());
File apkFilePath = ApkCache.getApkDownloadPath(activity, apkDownloadUri);
Utils.debugLog(TAG, "skip download, we have already downloaded " + currentStatus.apk.getUrl() +
" to " + apkFilePath);
// TODO: This seems like a bit of a hack. Is there a better way to do this by changing
// the Installer API so that we can ask it to install without having to get it to fire
// off an intent which we then listen for and action?
final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(activity);
final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
broadcastManager.unregisterReceiver(this);
if (Installer.ACTION_INSTALL_USER_INTERACTION.equals(intent.getAction())) {
PendingIntent pendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
try {
pendingIntent.send();
} catch (PendingIntent.CanceledException ignored) { }
}
}
};
broadcastManager.registerReceiver(receiver, Installer.getInstallIntentFilter(apkDownloadUri));
Installer installer = InstallerFactory.create(activity, currentStatus.apk);
installer.installPackage(Uri.parse(apkFilePath.toURI().toString()), apkDownloadUri);
} else {
final Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, app);
InstallManagerService.queue(activity, app, suggestedApk);
}
}
@SuppressWarnings("FieldCanBeLocal")
private final View.OnClickListener onCancelDownload = new View.OnClickListener() {
@Override

View File

@ -5,6 +5,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
@ -20,6 +21,7 @@ import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.views.updates.items.AppStatus;
import org.fdroid.fdroid.views.updates.items.AppUpdateData;
import org.fdroid.fdroid.views.updates.items.KnownVulnApp;
import org.fdroid.fdroid.views.updates.items.UpdateableApp;
import org.fdroid.fdroid.views.updates.items.UpdateableAppsHeader;
@ -65,6 +67,9 @@ import java.util.Set;
public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final int LOADER_CAN_UPDATE = 289753982;
private static final int LOADER_KNOWN_VULN = 520389740;
private final AdapterDelegatesManager<List<AppUpdateData>> delegatesManager = new AdapterDelegatesManager<>();
private final List<AppUpdateData> items = new ArrayList<>();
@ -72,6 +77,7 @@ public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
private final List<AppStatus> appsToShowStatus = new ArrayList<>();
private final List<UpdateableApp> updateableApps = new ArrayList<>();
private final List<KnownVulnApp> knownVulnApps = new ArrayList<>();
private boolean showAllUpdateableApps = false;
@ -80,9 +86,11 @@ public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
delegatesManager.addDelegate(new AppStatus.Delegate(activity))
.addDelegate(new UpdateableApp.Delegate(activity))
.addDelegate(new UpdateableAppsHeader.Delegate(activity));
.addDelegate(new UpdateableAppsHeader.Delegate(activity))
.addDelegate(new KnownVulnApp.Delegate(activity));
activity.getSupportLoaderManager().initLoader(0, null, this);
activity.getSupportLoaderManager().initLoader(LOADER_CAN_UPDATE, null, this);
activity.getSupportLoaderManager().initLoader(LOADER_KNOWN_VULN, null, this);
}
/**
@ -162,6 +170,10 @@ public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
}
}
}
for (KnownVulnApp app : knownVulnApps) {
items.add(app);
}
}
@Override
@ -186,33 +198,41 @@ public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri uri;
switch (id) {
case LOADER_CAN_UPDATE:
uri = AppProvider.getCanUpdateUri();
break;
case LOADER_KNOWN_VULN:
uri = AppProvider.getInstalledWithKnownVulnsUri();
break;
default:
throw new IllegalStateException("Unknown loader requested: " + id);
}
return new CursorLoader(
activity,
AppProvider.getCanUpdateUri(),
new String[]{
Schema.AppMetadataTable.Cols._ID, // Required for cursor loader to work.
Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME,
Schema.AppMetadataTable.Cols.NAME,
Schema.AppMetadataTable.Cols.SUMMARY,
Schema.AppMetadataTable.Cols.IS_COMPATIBLE,
Schema.AppMetadataTable.Cols.LICENSE,
Schema.AppMetadataTable.Cols.ICON,
Schema.AppMetadataTable.Cols.ICON_URL,
Schema.AppMetadataTable.Cols.InstalledApp.VERSION_CODE,
Schema.AppMetadataTable.Cols.InstalledApp.VERSION_NAME,
Schema.AppMetadataTable.Cols.SuggestedApk.VERSION_NAME,
Schema.AppMetadataTable.Cols.SUGGESTED_VERSION_CODE,
Schema.AppMetadataTable.Cols.REQUIREMENTS, // Needed for filtering apps that require root.
Schema.AppMetadataTable.Cols.ANTI_FEATURES, // Needed for filtering apps that require anti-features.
},
null,
null,
Schema.AppMetadataTable.Cols.NAME
);
activity, uri, Schema.AppMetadataTable.Cols.ALL, null, null, Schema.AppMetadataTable.Cols.NAME);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
switch (loader.getId()) {
case LOADER_CAN_UPDATE:
onCanUpdateLoadFinished(cursor);
break;
case LOADER_KNOWN_VULN:
onKnownVulnLoadFinished(cursor);
break;
}
populateItems();
notifyDataSetChanged();
}
private void onCanUpdateLoadFinished(Cursor cursor) {
updateableApps.clear();
cursor.moveToFirst();
@ -220,9 +240,16 @@ public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
updateableApps.add(new UpdateableApp(activity, new App(cursor)));
cursor.moveToNext();
}
}
populateItems();
notifyDataSetChanged();
private void onKnownVulnLoadFinished(Cursor cursor) {
knownVulnApps.clear();
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
knownVulnApps.add(new KnownVulnApp(activity, new App(cursor)));
cursor.moveToNext();
}
}
@Override

View File

@ -0,0 +1,60 @@
package org.fdroid.fdroid.views.updates.items;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
import com.hannesdorfmann.adapterdelegates3.AdapterDelegate;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import java.util.List;
/**
* List of all apps which can be updated, but have not yet been downloaded.
*
* @see KnownVulnApp The data that is bound to this view.
* @see R.layout#known_vuln_app_list_item The view that this binds to.
* @see KnownVulnAppListItemController Used for binding the {@link App} to
* the {@link R.layout#known_vuln_app_list_item}
*/
public class KnownVulnApp extends AppUpdateData {
public final App app;
public KnownVulnApp(Activity activity, App app) {
super(activity);
this.app = app;
}
public static class Delegate extends AdapterDelegate<List<AppUpdateData>> {
private final Activity activity;
public Delegate(Activity activity) {
this.activity = activity;
}
@Override
protected boolean isForViewType(@NonNull List<AppUpdateData> items, int position) {
return items.get(position) instanceof KnownVulnApp;
}
@NonNull
@Override
protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new KnownVulnAppListItemController(activity, activity.getLayoutInflater()
.inflate(R.layout.known_vuln_app_list_item, parent, false));
}
@Override
protected void onBindViewHolder(@NonNull List<AppUpdateData> items, int position,
@NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
KnownVulnApp app = (KnownVulnApp) items.get(position);
((KnownVulnAppListItemController) holder).bindModel(app.app);
}
}
}

View File

@ -0,0 +1,78 @@
package org.fdroid.fdroid.views.updates.items;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.view.View;
import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.views.apps.AppListItemController;
import org.fdroid.fdroid.views.apps.AppListItemState;
/**
* Tell the user that an app they have installed has a known vulnerability.
* The role of this controller is to prompt the user what it is that should be done in response to this
* (e.g. uninstall, update, disable).
*/
public class KnownVulnAppListItemController extends AppListItemController {
public KnownVulnAppListItemController(Activity activity, View itemView) {
super(activity, itemView);
}
@NonNull
@Override
protected AppListItemState getCurrentViewState(
@NonNull App app, @Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) {
return new AppListItemState(app)
.setMainText(activity.getString(R.string.updates__app_with_known_vulnerability__uninstall, app.name))
.showActionButton(activity.getString(R.string.menu_uninstall));
}
@Override
protected void onActionButtonPressed(@NonNull App app) {
LocalBroadcastManager.getInstance(activity).registerReceiver(uninstallReceiver,
Installer.getUninstallIntentFilter(app.packageName));
InstallerService.uninstall(activity, app.getInstalledApk(activity));
}
private void unregisterUninstallReceiver() {
LocalBroadcastManager.getInstance(activity).unregisterReceiver(uninstallReceiver);
}
private final BroadcastReceiver uninstallReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch (intent.getAction()) {
case Installer.ACTION_UNINSTALL_COMPLETE:
// This will cause the LoaderManager in UpdatesAdapter to automatically requery for the list of
// apps with known vulnerabilities (i.e. this app should no longer be in that list).
activity.getContentResolver().notifyChange(AppProvider.getInstalledWithKnownVulnsUri(), null);
unregisterUninstallReceiver();
break;
case Installer.ACTION_UNINSTALL_INTERRUPTED:
unregisterUninstallReceiver();
break;
case Installer.ACTION_UNINSTALL_USER_INTERACTION:
PendingIntent uninstallPendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
try {
uninstallPendingIntent.send();
} catch (PendingIntent.CanceledException ignored) { }
break;
}
}
};
}

View File

@ -0,0 +1,19 @@
<vector android:height="24dp" android:viewportHeight="12.7"
android:viewportWidth="12.7" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M6.35,6.35m-6.35,0a6.35,6.35 0,1 1,12.7 0a6.35,6.35 0,1 1,-12.7 0"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0.52916664"/>
<path android:fillAlpha="1" android:fillColor="#ff3600"
android:pathData="M6.35,6.35m-5.33,0a5.33,5.33 0,1 1,10.661 0a5.33,5.33 0,1 1,-10.661 0"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0.52916664"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M5.863,3.778h0.904v3.082h-0.904z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0.52916664"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M5.863,8.899l0.904,0l0,-0.973l-0.904,0z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0.52916664"/>
</vector>

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="8dp"
android:clipToPadding="false">
<!-- Ignore ContentDescription because it is kind of meaningless to have TTS read out "App icon"
when it will inevitably read out the name of the app straight after. -->
<ImageView
android:id="@+id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="48dp"
android:layout_height="48dp"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
tools:ignore="ContentDescription" />
<ImageView
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="48dp"
android:layout_marginLeft="48dp"
android:layout_marginTop="40dp"
android:src="@drawable/ic_known_vuln_overlay"
android:scaleType="fitCenter"
tools:ignore="ContentDescription"/>
<TextView
android:id="@+id/app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="We found a vulnerability with VulnApp. We recommend uninstalling this app immediately."
android:textSize="16sp"
android:textColor="?attr/installedApps"
android:ellipsize="end"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
android:layout_marginRight="8dp" />
<Button
android:id="@+id/action_button"
style="@style/DetailsPrimaryButtonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/app_name"
tools:text="Uninstall"/>
</android.support.constraint.ConstraintLayout>

View File

@ -97,9 +97,10 @@ This often occurs with apps installed via Google Play or other sources, if they
<string name="updates__tts__download_app">Download</string>
<string name="updates__tts__download_updates_for_all_apps">Download all updates</string>
<string name="updates__app_with_known_vulnerability__uninstall">We found a vulnerability with %1$s. We recommend uninstalling this app immediately.</string>
<string name="updates__hide_updateable_apps">Hide apps</string>
<string name="updates__show_updateable_apps">Show apps</string>
<plurals name="updates__download_updates_for_apps">
<item quantity="one">Download update for %1$d app.</item>
<item quantity="other">Download updates for %1$d apps.</item>

View File

@ -21,6 +21,7 @@ import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.FDroidProviderTest;
import org.fdroid.fdroid.data.InstalledAppTestUtils;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.RepoPushRequest;
@ -121,6 +122,12 @@ public class IndexV1UpdaterTest extends FDroidProviderTest {
assertEquals("repo.mirrors should have items", 2, repo.mirrors.length);
assertEquals("repo.mirrors first URL", "http://frkcchxlcvnb4m5a.onion/fdroid/repo", repo.mirrors[0]);
assertEquals("repo.mirrors second URL", "http://testy.at.or.at/fdroid/repo", repo.mirrors[1]);
// Make sure the per-apk anti features which are new in index v1 get added correctly.
assertEquals(0, AppProvider.Helper.findInstalledAppsWithKnownVulns(context).size());
InstalledAppTestUtils.install(context, "com.waze", 1019841, "v3.9.5.4", "362488e7be5ea0689b4e97d989ae1404",
"cbbdb8c5dafeccd7dd7b642dde0477d3489e18ac366e3c8473d5c07e5f735a95");
assertEquals(1, AppProvider.Helper.findInstalledAppsWithKnownVulns(context).size());
}
@Test(expected = RepoUpdater.SigningException.class)