diff --git a/app/src/full/AndroidManifest.xml b/app/src/full/AndroidManifest.xml index a39ec62e8..7835b76a9 100644 --- a/app/src/full/AndroidManifest.xml +++ b/app/src/full/AndroidManifest.xml @@ -104,6 +104,10 @@ + + wipeSet = Preferences.get().getPanicWipeSet(); + categoryAppsToUninstall.removeAll(); + if (Panic.PACKAGE_NAME_NONE.equals(prefApp.getValue())) { + categoryAppsToUninstall.setEnabled(false); + return; + } + categoryAppsToUninstall.setEnabled(true); + if (wipeSet.size() > 0) { + for (String packageName : wipeSet) { + Preference preference = new Preference(getActivity()); + preference.setSingleLineTitle(true); + preference.setIntent(intent); + categoryAppsToUninstall.addPreference(preference); + try { + preference.setTitle(pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0))); + preference.setIcon(pm.getApplicationIcon(packageName)); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + preference.setTitle(packageName); + } + } + } else { + Preference preference = new Preference(getActivity()); + preference.setIntent(intent); + Drawable icon = getResources().getDrawable(R.drawable.ic_add_circle_outline_white); + icon.setColorFilter(new LightingColorFilter(0, getResources().getColor(R.color.swap_light_grey_icon))); + preference.setSingleLineTitle(true); + preference.setTitle(R.string.panic_add_apps_to_uninstall); + preference.setIcon(icon); + categoryAppsToUninstall.addPreference(preference); + } } @Override @@ -156,6 +204,7 @@ public class PanicPreferencesFragment extends PreferenceFragment // disable destructive panic actions prefHide.setEnabled(false); + showWipeList(); } else { // try to display connected panic app try { @@ -163,6 +212,7 @@ public class PanicPreferencesFragment extends PreferenceFragment prefApp.setSummary(pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0))); prefApp.setIcon(pm.getApplicationIcon(packageName)); prefHide.setEnabled(true); + showWipeList(); } catch (PackageManager.NameNotFoundException e) { // revert back to no app, just to be safe PanicResponder.setTriggerPackageName(getActivity(), Panic.PACKAGE_NAME_NONE); diff --git a/app/src/full/java/org/fdroid/fdroid/views/panic/SelectInstalledAppListAdapter.java b/app/src/full/java/org/fdroid/fdroid/views/panic/SelectInstalledAppListAdapter.java new file mode 100644 index 000000000..e0c49c927 --- /dev/null +++ b/app/src/full/java/org/fdroid/fdroid/views/panic/SelectInstalledAppListAdapter.java @@ -0,0 +1,30 @@ +package org.fdroid.fdroid.views.panic; + +import android.app.Activity; +import android.support.annotation.NonNull; +import android.view.View; +import android.view.ViewGroup; +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.views.installed.InstalledAppListAdapter; +import org.fdroid.fdroid.views.installed.InstalledAppListItemController; + +import java.util.Set; + +public class SelectInstalledAppListAdapter extends InstalledAppListAdapter { + private final Set selectedApps; + + SelectInstalledAppListAdapter(Activity activity) { + super(activity); + Preferences prefs = Preferences.get(); + selectedApps = prefs.getPanicWipeSet(); + prefs.setPanicTmpSelectedSet(selectedApps); + } + + @NonNull + @Override + public InstalledAppListItemController onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = activity.getLayoutInflater().inflate(R.layout.installed_app_list_item, parent, false); + return new SelectInstalledAppListItemController(activity, view, selectedApps); + } +} diff --git a/app/src/full/java/org/fdroid/fdroid/views/panic/SelectInstalledAppListItemController.java b/app/src/full/java/org/fdroid/fdroid/views/panic/SelectInstalledAppListItemController.java new file mode 100644 index 000000000..eac2c9f2d --- /dev/null +++ b/app/src/full/java/org/fdroid/fdroid/views/panic/SelectInstalledAppListItemController.java @@ -0,0 +1,37 @@ +package org.fdroid.fdroid.views.panic; + +import android.app.Activity; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; +import org.fdroid.fdroid.AppUpdateStatusManager; +import org.fdroid.fdroid.data.App; +import org.fdroid.fdroid.views.apps.AppListItemState; +import org.fdroid.fdroid.views.installed.InstalledAppListItemController; + +import java.util.Set; + +/** + * Shows the currently installed apps as a selectable list. + */ +public class SelectInstalledAppListItemController extends InstalledAppListItemController { + + private final Set selectedApps; + + public SelectInstalledAppListItemController(Activity activity, View itemView, Set selectedApps) { + super(activity, itemView); + this.selectedApps = selectedApps; + } + + @NonNull + @Override + protected AppListItemState getCurrentViewState( + @NonNull App app, @Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) { + return new AppListItemState(app).setCheckBoxStatus(selectedApps.contains(app.packageName)); + } + + @Override + protected void onActionButtonPressed(App app) { + super.onActionButtonPressed(app); + } +} diff --git a/app/src/full/java/org/fdroid/fdroid/views/panic/SelectInstalledAppsActivity.java b/app/src/full/java/org/fdroid/fdroid/views/panic/SelectInstalledAppsActivity.java new file mode 100644 index 000000000..3d222cff9 --- /dev/null +++ b/app/src/full/java/org/fdroid/fdroid/views/panic/SelectInstalledAppsActivity.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2010-12 Ciaran Gultnieks, ciaran@ciarang.com + * Copyright (C) 2009 Roberto Jacinto, roberto.jacinto@caixamagica.pt + * + * 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.views.panic; + +import android.annotation.SuppressLint; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.LightingColorFilter; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.v4.app.LoaderManager; +import android.support.v4.content.CursorLoader; +import android.support.v4.content.Loader; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.Toolbar; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.TextView; +import org.fdroid.fdroid.FDroidApp; +import org.fdroid.fdroid.Preferences; +import org.fdroid.fdroid.R; +import org.fdroid.fdroid.data.AppProvider; +import org.fdroid.fdroid.data.Schema; +import org.fdroid.fdroid.views.installed.InstalledAppListAdapter; + +public class SelectInstalledAppsActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks { + + private InstalledAppListAdapter adapter; + private RecyclerView appList; + private TextView emptyState; + private int checkId; + + private Preferences prefs; + + @Override + protected void onCreate(Bundle savedInstanceState) { + + ((FDroidApp) getApplication()).applyTheme(this); + super.onCreate(savedInstanceState); + + setContentView(R.layout.installed_apps_layout); + + Toolbar toolbar = findViewById(R.id.toolbar); + toolbar.setTitle(getString(R.string.panic_add_apps_to_uninstall)); + setSupportActionBar(toolbar); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + adapter = new SelectInstalledAppListAdapter(this); + + appList = findViewById(R.id.app_list); + appList.setHasFixedSize(true); + appList.setLayoutManager(new LinearLayoutManager(this)); + appList.setAdapter(adapter); + + emptyState = findViewById(R.id.empty_state); + } + + @Override + protected void onResume() { + super.onResume(); + + prefs = Preferences.get(); + + // Starts a new or restarts an existing Loader in this manager + getSupportLoaderManager().restartLoader(0, null, this); + } + + @NonNull + @Override + public Loader onCreateLoader(int id, Bundle args) { + return new CursorLoader( + this, + AppProvider.getInstalledUri(), + Schema.AppMetadataTable.Cols.ALL, + null, null, null); + } + + @Override + public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { + adapter.setApps(cursor); + + if (adapter.getItemCount() == 0) { + appList.setVisibility(View.GONE); + emptyState.setVisibility(View.VISIBLE); + } else { + appList.setVisibility(View.VISIBLE); + emptyState.setVisibility(View.GONE); + } + } + + @Override + public void onLoaderReset(@NonNull Loader loader) { + adapter.setApps(null); + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuItem menuItem = menu.add(R.string.menu_select_for_wipe); + menuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + checkId = menuItem.getItemId(); + if (FDroidApp.isAppThemeLight()) { + Resources resources = getResources(); + Drawable icon = resources.getDrawable(R.drawable.check); + icon.setColorFilter(new LightingColorFilter(0xffffffff, resources.getColor(android.R.color.white))); + menuItem.setIcon(icon); + } else { + menuItem.setIcon(R.drawable.check); + } + return true; + } + + @SuppressLint("ApplySharedPref") + @Override + public boolean onOptionsItemSelected(MenuItem item) { + finish(); + if (item.getItemId() == checkId) { + prefs.setPanicWipeSet(prefs.getPanicTmpSelectedSet()); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/full/res/drawable/check.xml b/app/src/full/res/drawable/check.xml new file mode 100644 index 000000000..064f846ae --- /dev/null +++ b/app/src/full/res/drawable/check.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/full/res/xml/preferences_panic.xml b/app/src/full/res/xml/preferences_panic.xml index e26d3eabb..79f9925fc 100644 --- a/app/src/full/res/xml/preferences_panic.xml +++ b/app/src/full/res/xml/preferences_panic.xml @@ -28,4 +28,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/org/fdroid/fdroid/Preferences.java b/app/src/main/java/org/fdroid/fdroid/Preferences.java index 46dc29b3b..e129bb7db 100644 --- a/app/src/main/java/org/fdroid/fdroid/Preferences.java +++ b/app/src/main/java/org/fdroid/fdroid/Preferences.java @@ -35,10 +35,12 @@ import org.fdroid.fdroid.installer.PrivilegedInstaller; import org.fdroid.fdroid.net.ConnectivityMonitorService; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Random; +import java.util.Set; import java.util.concurrent.TimeUnit; /** @@ -107,6 +109,8 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh public static final String PREF_PREVENT_SCREENSHOTS = "preventScreenshots"; public static final String PREF_PANIC_EXIT = "pref_panic_exit"; public static final String PREF_PANIC_HIDE = "pref_panic_hide"; + public static final String PREF_PANIC_WIPE_SET = "panicWipeSet"; + public static final String PREF_PANIC_TMP_SELECTED_SET = "panicTmpSelectedSet"; public static final String PREF_HIDE_ON_LONG_PRESS_SEARCH = "hideOnLongPressSearch"; public static final String PREF_HIDE_ALL_NOTIFICATIONS = "hideAllNotifications"; public static final String PREF_SEND_VERSION_AND_UUID_TO_SERVERS = "sendVersionAndUUIDToServers"; @@ -515,6 +519,22 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh return preferences.getBoolean(PREF_HIDE_ON_LONG_PRESS_SEARCH, IGNORED_B); } + public Set getPanicTmpSelectedSet() { + return preferences.getStringSet(Preferences.PREF_PANIC_TMP_SELECTED_SET, Collections.emptySet()); + } + + public void setPanicTmpSelectedSet(Set selectedSet) { + preferences.edit().putStringSet(Preferences.PREF_PANIC_TMP_SELECTED_SET, selectedSet).apply(); + } + + public Set getPanicWipeSet() { + return preferences.getStringSet(Preferences.PREF_PANIC_WIPE_SET, Collections.emptySet()); + } + + public void setPanicWipeSet(Set selectedSet) { + preferences.edit().putStringSet(Preferences.PREF_PANIC_WIPE_SET, selectedSet).apply(); + } + /** * Preference for whitelabel builds that are meant to be entirely controlled * by the server, without user interaction, e.g. "appliances". diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java index ad434b619..e8825bbbf 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemController.java @@ -22,15 +22,15 @@ import android.text.TextUtils; import android.view.View; import android.view.ViewOutlineProvider; import android.widget.Button; +import android.widget.CheckBox; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ProgressBar; import android.widget.TextView; - import com.nostra13.universalimageloader.core.ImageLoader; - import org.fdroid.fdroid.AppUpdateStatusManager; import org.fdroid.fdroid.AppUpdateStatusManager.AppUpdateStatus; +import org.fdroid.fdroid.Preferences; import org.fdroid.fdroid.R; import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.Apk; @@ -45,6 +45,7 @@ import org.fdroid.fdroid.views.updates.UpdatesAdapter; import java.io.File; import java.util.Iterator; +import java.util.Set; /** * Supports the following layouts: @@ -64,6 +65,8 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { private static final String TAG = "AppListItemController"; + private static Preferences prefs; + protected final Activity activity; @NonNull @@ -97,6 +100,9 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { @Nullable private final Button secondaryButton; + @Nullable + private final CheckBox checkBox; + @Nullable private App currentApp; @@ -107,6 +113,9 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { public AppListItemController(final Activity activity, View itemView) { super(itemView); this.activity = activity; + if (prefs == null) { + prefs = Preferences.get(); + } installButton = (ImageView) itemView.findViewById(R.id.install); if (installButton != null) { @@ -145,6 +154,7 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { cancelButton = (ImageButton) itemView.findViewById(R.id.cancel_button); actionButton = (Button) itemView.findViewById(R.id.action_button); secondaryButton = (Button) itemView.findViewById(R.id.secondary_button); + checkBox = itemView.findViewById(R.id.checkbox); if (actionButton != null) { actionButton.setEnabled(true); @@ -220,9 +230,9 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { /** * Override to respond to the user swiping an app to dismiss it from the list. - * @param app The app that was swiped away - * @param updatesAdapter The adapter. Can be used for refreshing the adapter with adapter.refreshStatuses(). * + * @param app The app that was swiped away + * @param updatesAdapter The adapter. Can be used for refreshing the adapter with adapter.refreshStatuses(). * @see #canDismiss() This must also be overridden and should return true. */ protected void onDismissApp(@NonNull App app, UpdatesAdapter updatesAdapter) { @@ -328,6 +338,18 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { secondaryStatus.setText(statusText); } } + + if (checkBox != null) { + if (viewState.shouldShowCheckBox()) { + itemView.setOnClickListener(selectInstalledAppListener); + checkBox.setChecked(viewState.isCheckBoxChecked()); + checkBox.setVisibility(View.VISIBLE); + status.setVisibility(View.GONE); + secondaryStatus.setVisibility(View.GONE); + } else { + checkBox.setVisibility(View.GONE); + } + } } @NonNull @@ -533,4 +555,18 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder { InstallManagerService.cancel(activity, currentStatus.getCanonicalUrl()); } + + private final View.OnClickListener selectInstalledAppListener = new View.OnClickListener() { + @Override + public void onClick(View v) { + Set wipeSet = prefs.getPanicTmpSelectedSet(); + checkBox.toggle(); + if (checkBox.isChecked()) { + wipeSet.add(currentApp.packageName); + } else { + wipeSet.remove(currentApp.packageName); + } + prefs.setPanicTmpSelectedSet(wipeSet); + } + }; } diff --git a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemState.java b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemState.java index 2fef4de81..464626fe2 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemState.java +++ b/app/src/main/java/org/fdroid/fdroid/views/apps/AppListItemState.java @@ -2,12 +2,12 @@ package org.fdroid.fdroid.views.apps; import android.support.annotation.NonNull; import android.support.annotation.Nullable; - import org.fdroid.fdroid.Utils; import org.fdroid.fdroid.data.App; /** * A dumb model which is used to specify what should/should not be shown in an {@link AppListItemController}. + * * @see AppListItemController and its subclasses. */ public class AppListItemState { @@ -20,6 +20,8 @@ public class AppListItemState { private int progressCurrent = -1; private int progressMax = -1; private boolean showInstallButton; + private boolean showCheckBox; + private boolean isCheckBoxChecked; public AppListItemState(@NonNull App app) { this.app = app; @@ -63,6 +65,9 @@ public class AppListItemState { @Nullable public CharSequence getMainText() { + if (showCheckBox) { + return app.name; + } return mainText != null ? mainText : Utils.formatAppNameAndSummary(app.name, app.summary); @@ -113,4 +118,23 @@ public class AppListItemState { public CharSequence getSecondaryStatusText() { return secondaryStatusText; } + + public boolean shouldShowCheckBox() { + return showCheckBox; + } + + public boolean isCheckBoxChecked() { + return isCheckBoxChecked; + } + + /** + * Enable the {@link android.widget.CheckBox} display and set the on/off status + * e.g. {@link android.widget.CheckBox#isChecked()} + */ + public AppListItemState setCheckBoxStatus(boolean checked) { + this.showCheckBox = true; + this.isCheckBoxChecked = checked; + return this; + } + } diff --git a/app/src/main/java/org/fdroid/fdroid/views/installed/InstalledAppListAdapter.java b/app/src/main/java/org/fdroid/fdroid/views/installed/InstalledAppListAdapter.java index 0667afb96..b5ca5d692 100644 --- a/app/src/main/java/org/fdroid/fdroid/views/installed/InstalledAppListAdapter.java +++ b/app/src/main/java/org/fdroid/fdroid/views/installed/InstalledAppListAdapter.java @@ -11,14 +11,14 @@ import org.fdroid.fdroid.R; import org.fdroid.fdroid.data.App; import org.fdroid.fdroid.data.Schema; -class InstalledAppListAdapter extends RecyclerView.Adapter { +public class InstalledAppListAdapter extends RecyclerView.Adapter { - private final Activity activity; + protected final Activity activity; @Nullable private Cursor cursor; - InstalledAppListAdapter(Activity activity) { + protected InstalledAppListAdapter(Activity activity) { this.activity = activity; setHasStableIds(true); } diff --git a/app/src/main/res/layout/installed_app_list_item.xml b/app/src/main/res/layout/installed_app_list_item.xml index e65779c86..87b44a3a4 100644 --- a/app/src/main/res/layout/installed_app_list_item.xml +++ b/app/src/main/res/layout/installed_app_list_item.xml @@ -71,4 +71,14 @@ android:layout_marginStart="8dp" android:layout_marginLeft="8dp" /> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4c32706a1..e67a1b3b6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -206,6 +206,7 @@ This often occurs with apps installed via Google Play or other sources, if they Install Uninstall + Select for wipe Ignore All Updates Ignore This Update Website @@ -323,9 +324,12 @@ This often occurs with apps installed via Google Play or other sources, if they Panic button settings Actions to be taken in case of emergency - Exit App - App will be closed + Exit app + This app will be closed Destructive Actions + Will be uninstalled and all data deleted + Apps to be uninstalled and all data wiped + Add apps to be uninstalled and wiped Hide %s App will hide itself Remember how to restore