Merge branch 'panic-app-wipe' into 'master'

panic app and repo wipe

See merge request fdroid/fdroidclient!827
This commit is contained in:
Hans-Christoph Steiner 2019-06-13 10:37:37 +00:00
commit ca81518f74
33 changed files with 832 additions and 111 deletions

View File

@ -148,7 +148,7 @@ dependencies {
implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
implementation 'com.google.zxing:core:3.3.3'
implementation 'info.guardianproject.netcipher:netcipher:2.0.0-beta1'
implementation 'info.guardianproject.panic:panic:0.5'
implementation 'info.guardianproject.panic:panic:1.0'
implementation 'commons-io:commons-io:2.6'
implementation 'commons-net:commons-net:3.6'
implementation 'ch.acra:acra:4.9.1'

View File

@ -104,6 +104,10 @@
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name=".views.panic.SelectInstalledAppsActivity"
android:parentActivityName=".views.panic.PanicPreferencesActivity"/>
<activity
android:name=".views.panic.PanicResponderActivity"
android:noHistory="true"

View File

@ -0,0 +1,41 @@
package org.fdroid.fdroid.views.panic;
import android.content.Context;
import android.content.res.Resources;
import android.support.v7.preference.CheckBoxPreference;
import android.support.v7.preference.PreferenceViewHolder;
import android.util.AttributeSet;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
public class DestructiveCheckBoxPreference extends CheckBoxPreference {
public DestructiveCheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public DestructiveCheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public DestructiveCheckBoxPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DestructiveCheckBoxPreference(Context context) {
super(context);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
if (!holder.itemView.isEnabled()) {
return;
}
Resources resources = getContext().getResources();
if (FDroidApp.isAppThemeLight()) {
holder.itemView.setBackgroundColor(resources.getColor(R.color.panic_destructive_light));
} else {
holder.itemView.setBackgroundColor(resources.getColor(R.color.panic_destructive_dark));
}
}
}

View File

@ -0,0 +1,36 @@
package org.fdroid.fdroid.views.panic;
import android.content.Context;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceViewHolder;
import android.util.AttributeSet;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
public class DestructivePreference extends Preference {
public DestructivePreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public DestructivePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public DestructivePreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DestructivePreference(Context context) {
super(context);
}
@Override
public void onBindViewHolder(PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
if (FDroidApp.isAppThemeLight()) {
holder.itemView.setBackgroundColor(getContext().getResources().getColor(R.color.panic_destructive_light));
} else {
holder.itemView.setBackgroundColor(getContext().getResources().getColor(R.color.panic_destructive_dark));
}
}
}

View File

@ -8,6 +8,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.graphics.LightingColorFilter;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.net.Uri;
@ -20,37 +21,42 @@ import android.support.v7.app.AlertDialog;
import android.support.v7.preference.CheckBoxPreference;
import android.support.v7.preference.ListPreference;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceCategory;
import android.text.TextUtils;
import android.util.TypedValue;
import info.guardianproject.panic.Panic;
import info.guardianproject.panic.PanicResponder;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.installer.PrivilegedInstaller;
import org.fdroid.fdroid.views.hiding.HidingManager;
import java.util.ArrayList;
import java.util.Set;
public class PanicPreferencesFragment extends PreferenceFragment
implements SharedPreferences.OnSharedPreferenceChangeListener {
private static final String PREF_EXIT = Preferences.PREF_PANIC_EXIT;
private static final String PREF_APP = "pref_panic_app";
private static final String PREF_HIDE = Preferences.PREF_PANIC_HIDE;
private PackageManager pm;
private ListPreference prefApp;
private CheckBoxPreference prefExit;
private CheckBoxPreference prefHide;
private CheckBoxPreference prefResetRepos;
private PreferenceCategory categoryAppsToUninstall;
@Override
public void onCreatePreferences(Bundle bundle, String s) {
addPreferencesFromResource(R.xml.preferences_panic);
pm = getActivity().getPackageManager();
prefExit = (CheckBoxPreference) findPreference(PREF_EXIT);
prefExit = (CheckBoxPreference) findPreference(Preferences.PREF_PANIC_EXIT);
prefApp = (ListPreference) findPreference(PREF_APP);
prefHide = (CheckBoxPreference) findPreference(PREF_HIDE);
prefHide = (CheckBoxPreference) findPreference(Preferences.PREF_PANIC_HIDE);
prefHide.setTitle(getString(R.string.panic_hide_title, getString(R.string.app_name)));
prefResetRepos = (CheckBoxPreference) findPreference(Preferences.PREF_PANIC_RESET_REPOS);
categoryAppsToUninstall = (PreferenceCategory) findPreference("pref_panic_apps_to_uninstall");
if (PanicResponder.checkForDisconnectIntent(getActivity())) {
// the necessary action should have been performed by the check already
@ -73,9 +79,12 @@ public class PanicPreferencesFragment extends PreferenceFragment
if (packageName.equals(Panic.PACKAGE_NAME_NONE)) {
prefHide.setChecked(false);
prefHide.setEnabled(false);
prefResetRepos.setChecked(false);
prefResetRepos.setEnabled(false);
getActivity().setResult(Activity.RESULT_CANCELED);
} else {
prefHide.setEnabled(true);
prefResetRepos.setEnabled(true);
}
showPanicApp(packageName);
return true;
@ -88,6 +97,48 @@ public class PanicPreferencesFragment extends PreferenceFragment
public void onStart() {
super.onStart();
getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
if (!PrivilegedInstaller.isDefault(getActivity())) {
getPreferenceScreen().removePreference(categoryAppsToUninstall);
return;
}
showWipeList();
}
private void showWipeList() {
Intent intent = new Intent(getActivity(), SelectInstalledAppsActivity.class);
intent.setAction(Intent.ACTION_MAIN);
Set<String> 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 DestructivePreference(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
@ -98,11 +149,13 @@ public class PanicPreferencesFragment extends PreferenceFragment
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(PREF_HIDE) && sharedPreferences.getBoolean(PREF_HIDE, false)) {
if (key.equals(Preferences.PREF_PANIC_HIDE)
&& sharedPreferences.getBoolean(Preferences.PREF_PANIC_HIDE, false)) {
showHideConfirmationDialog();
}
// disable "hiding" if "exit" gets disabled
if (key.equals(PREF_EXIT) && !sharedPreferences.getBoolean(PREF_EXIT, true)) {
if (key.equals(Preferences.PREF_PANIC_EXIT)
&& !sharedPreferences.getBoolean(Preferences.PREF_PANIC_EXIT, true)) {
prefHide.setChecked(false);
}
}
@ -156,6 +209,7 @@ public class PanicPreferencesFragment extends PreferenceFragment
// disable destructive panic actions
prefHide.setEnabled(false);
showWipeList();
} else {
// try to display connected panic app
try {
@ -163,6 +217,8 @@ public class PanicPreferencesFragment extends PreferenceFragment
prefApp.setSummary(pm.getApplicationLabel(pm.getApplicationInfo(packageName, 0)));
prefApp.setIcon(pm.getApplicationIcon(packageName));
prefHide.setEnabled(true);
prefResetRepos.setEnabled(true);
showWipeList();
} catch (PackageManager.NameNotFoundException e) {
// revert back to no app, just to be safe
PanicResponder.setTriggerPackageName(getActivity(), Panic.PACKAGE_NAME_NONE);
@ -243,6 +299,7 @@ public class PanicPreferencesFragment extends PreferenceFragment
@Override
public void onCancel(DialogInterface dialogInterface) {
prefHide.setChecked(false);
prefResetRepos.setChecked(false);
}
});
builder.setView(R.layout.dialog_app_hiding);

View File

@ -1,17 +1,48 @@
package org.fdroid.fdroid.views.panic;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.views.hiding.HidingManager;
import info.guardianproject.panic.Panic;
import info.guardianproject.panic.PanicResponder;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.DBHelper;
import org.fdroid.fdroid.data.InstalledApp;
import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.installer.Installer;
import org.fdroid.fdroid.installer.InstallerService;
import org.fdroid.fdroid.installer.PrivilegedInstaller;
import org.fdroid.fdroid.views.hiding.HidingManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* This {@link Activity} is purely to run events in response to a panic trigger.
* It needs to be an {@code Activity} rather than a {@link android.app.Service}
* so that it can fetch some of the required information about what sent the
* {@link Intent}. This is therefore an {@code Activity} without any UI, which
* is a special case in Android. All the code must be in
* {@link #onCreate(Bundle)} and {@link #finish()} must be called at the end of
* that method.
*
* @see PanicResponder#receivedTriggerFromConnectedApp(Activity)
*/
public class PanicResponderActivity extends AppCompatActivity {
private static final String TAG = PanicResponderActivity.class.getSimpleName();
@ -21,7 +52,7 @@ public class PanicResponderActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent == null || !Panic.isTriggerIntent(intent)) {
if (!Panic.isTriggerIntent(intent)) {
finish();
return;
}
@ -29,12 +60,71 @@ public class PanicResponderActivity extends AppCompatActivity {
// received intent from panic app
Log.i(TAG, "Received Panic Trigger...");
Preferences preferences = Preferences.get();
final Preferences preferences = Preferences.get();
if (PanicResponder.receivedTriggerFromConnectedApp(this)) {
Log.i(TAG, "Panic Trigger came from connected app");
boolean receivedTriggerFromConnectedApp = PanicResponder.receivedTriggerFromConnectedApp(this);
final boolean runningAppUninstalls = PrivilegedInstaller.isDefault(this);
// Performing destructive panic responses
ArrayList<String> wipeList = new ArrayList<>(preferences.getPanicWipeSet());
preferences.setPanicWipeSet(Collections.<String>emptySet());
preferences.setPanicTmpSelectedSet(Collections.<String>emptySet());
if (receivedTriggerFromConnectedApp && runningAppUninstalls && wipeList.size() > 0) {
// if this app (e.g. F-Droid) is to be deleted, do it last
if (wipeList.contains(getPackageName())) {
wipeList.remove(getPackageName());
wipeList.add(getPackageName());
}
final Context context = this;
final CountDownLatch latch = new CountDownLatch(1);
final LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
final String lastToUninstall = wipeList.get(wipeList.size() - 1);
final BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
switch ((intent.getAction())) {
case Installer.ACTION_UNINSTALL_INTERRUPTED:
case Installer.ACTION_UNINSTALL_COMPLETE:
latch.countDown();
break;
}
}
};
lbm.registerReceiver(receiver, Installer.getUninstallIntentFilter(lastToUninstall));
for (String packageName : wipeList) {
InstalledApp installedApp = InstalledAppProvider.Helper.findByPackageName(context, packageName);
InstallerService.uninstall(context, new Apk(installedApp));
}
// wait for apps to uninstall before triggering final responses
new Thread() {
@Override
public void run() {
try {
latch.await(10, TimeUnit.MINUTES);
} catch (InterruptedException e) {
// ignored
}
lbm.unregisterReceiver(receiver);
if (preferences.panicResetRepos()) {
resetRepos(context);
}
if (preferences.panicHide()) {
HidingManager.hide(context);
}
if (preferences.panicExit()) {
exitAndClear();
}
}
}.start();
} else if (receivedTriggerFromConnectedApp) {
if (preferences.panicResetRepos()) {
resetRepos(this);
}
// Performing destructive panic response
if (preferences.panicHide()) {
Log.i(TAG, "Hiding app...");
HidingManager.hide(this);
@ -42,13 +132,43 @@ public class PanicResponderActivity extends AppCompatActivity {
}
// exit and clear, if not deactivated
if (preferences.panicExit()) {
ExitActivity.exitAndRemoveFromRecentApps(this);
if (Build.VERSION.SDK_INT >= 21) {
finishAndRemoveTask();
}
if (!runningAppUninstalls && preferences.panicExit()) {
exitAndClear();
}
finish();
}
static void resetRepos(Context context) {
HashSet<String> enabledAddresses = new HashSet<>();
HashSet<String> disabledAddresses = new HashSet<>();
String[] defaultReposItems = DBHelper.loadInitialRepos(context).toArray(new String[0]);
for (int i = 1; i < defaultReposItems.length; i += DBHelper.REPO_XML_ITEM_COUNT) {
if ("1".equals(defaultReposItems[i + 3])) {
enabledAddresses.add(defaultReposItems[i]);
} else {
disabledAddresses.add(defaultReposItems[i]);
}
}
List<Repo> repos = RepoProvider.Helper.all(context);
for (Repo repo : repos) {
ContentValues values = new ContentValues(1);
if (enabledAddresses.contains(repo.address)) {
values.put(Schema.RepoTable.Cols.IN_USE, true);
RepoProvider.Helper.update(context, repo, values);
} else if (disabledAddresses.contains(repo.address)) {
values.put(Schema.RepoTable.Cols.IN_USE, false);
RepoProvider.Helper.update(context, repo, values);
} else {
RepoProvider.Helper.remove(context, repo.getId());
}
}
}
private void exitAndClear() {
ExitActivity.exitAndRemoveFromRecentApps(this);
if (Build.VERSION.SDK_INT >= 21) {
finishAndRemoveTask();
}
}
}

View File

@ -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<String> 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);
}
}

View File

@ -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<String> selectedApps;
public SelectInstalledAppListItemController(Activity activity, View itemView, Set<String> 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);
}
}

View File

@ -0,0 +1,139 @@
/*
* 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.InstalledAppProvider;
import org.fdroid.fdroid.views.installed.InstalledAppListAdapter;
public class SelectInstalledAppsActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> {
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<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(this, InstalledAppProvider.getAllAppsUri(), null, null, null, null);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> 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<Cursor> 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);
}
}

View File

@ -30,7 +30,6 @@ import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.InstalledAppProvider;
import org.fdroid.fdroid.data.Schema.InstalledAppTable;
import org.fdroid.fdroid.localrepo.LocalRepoService;
import org.fdroid.fdroid.localrepo.SwapService;
import org.fdroid.fdroid.localrepo.SwapView;
public class SelectAppsView extends SwapView implements LoaderManager.LoaderCallbacks<Cursor> {
@ -61,7 +60,7 @@ public class SelectAppsView extends SwapView implements LoaderManager.LoaderCall
listView = findViewById(R.id.list);
adapter = new AppListAdapter(listView, getContext(),
getContext().getContentResolver().query(InstalledAppProvider.getContentUri(),
InstalledAppTable.Cols.ALL, null, null, null));
null, null, null, null));
listView.setAdapter(adapter);
listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
@ -97,13 +96,7 @@ public class SelectAppsView extends SwapView implements LoaderManager.LoaderCall
} else {
uri = InstalledAppProvider.getSearchUri(currentFilterString);
}
return new CursorLoader(
getActivity(),
uri,
InstalledAppTable.Cols.ALL,
null,
null,
InstalledAppTable.Cols.APPLICATION_LABEL);
return new CursorLoader(getActivity(), uri, null, null, null, null);
}
@Override

View File

@ -0,0 +1,8 @@
<!-- drawable/check.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
</vector>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<resources>
<color name="swap_incompatible">#ff7900</color>
<color name="swap_light_blue">#27aae1</color>
@ -10,4 +10,6 @@
<color name="swap_grey_icon">#4a4a4a</color>
<color name="swap_light_grey_icon">#bbbbbb</color>
<color name="panic_destructive_light">#fff0f0</color>
<color name="panic_destructive_dark">#330000</color>
</resources>

View File

@ -19,13 +19,26 @@
android:title="@string/panic_app_setting_title"
tools:icon="@drawable/ic_cancel"/>
<android.support.v7.preference.CheckBoxPreference
<org.fdroid.fdroid.views.panic.DestructiveCheckBoxPreference
android:defaultValue="false"
android:enabled="false"
android:key="pref_panic_hide"
android:summary="@string/panic_hide_summary"
android:title="@string/panic_hide_title"/>
<org.fdroid.fdroid.views.panic.DestructiveCheckBoxPreference
android:defaultValue="false"
android:enabled="false"
android:key="pref_panic_reset_repos"
android:summary="@string/panic_reset_repos_summary"
android:title="@string/panic_reset_repos_title"/>
</android.support.v7.preference.PreferenceCategory>
<android.support.v7.preference.PreferenceCategory
android:key="pref_panic_apps_to_uninstall"
android:title="@string/panic_apps_to_uninstall">
</android.support.v7.preference.PreferenceCategory>
</android.support.v7.preference.PreferenceScreen>

View File

@ -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,9 @@ 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_RESET_REPOS = "pref_panic_reset_repos";
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";
@ -511,10 +516,30 @@ public final class Preferences implements SharedPreferences.OnSharedPreferenceCh
return preferences.getBoolean(PREF_PANIC_HIDE, IGNORED_B);
}
public boolean panicResetRepos() {
return preferences.getBoolean(PREF_PANIC_RESET_REPOS, IGNORED_B);
}
public boolean hideOnLongPressSearch() {
return preferences.getBoolean(PREF_HIDE_ON_LONG_PRESS_SEARCH, IGNORED_B);
}
public Set<String> getPanicTmpSelectedSet() {
return preferences.getStringSet(Preferences.PREF_PANIC_TMP_SELECTED_SET, Collections.<String>emptySet());
}
public void setPanicTmpSelectedSet(Set<String> selectedSet) {
preferences.edit().putStringSet(Preferences.PREF_PANIC_TMP_SELECTED_SET, selectedSet).apply();
}
public Set<String> getPanicWipeSet() {
return preferences.getStringSet(Preferences.PREF_PANIC_WIPE_SET, Collections.<String>emptySet());
}
public void setPanicWipeSet(Set<String> 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".

View File

@ -86,25 +86,38 @@ public class ApkProvider extends FDroidProvider {
* <li>If installed, limit to apks signed by the same signer as the installed apk.</li>
* <li>Otherwise, limit to apks signed by the "preferred" signer (see {@link App#preferredSigner}).</li>
* </ul>
* If all else fails, try to return some {@link Apk} that will install something,
* rather than returning a null and triggering a {@link NullPointerException}.
*/
@Nullable
public static Apk findSuggestedApk(Context context, App app) {
return findApkFromAnyRepo(context, app.packageName, app.suggestedVersionCode,
app.getMostAppropriateSignature());
String mostAppropriateSignature = app.getMostAppropriateSignature();
Apk apk = findApkFromAnyRepo(context, app.packageName, app.suggestedVersionCode,
mostAppropriateSignature);
if (apk == null && (mostAppropriateSignature == null || !app.isInstalled(context))) {
List<Apk> apks = findByPackageName(context, app.packageName);
for (Apk availableApk : apks) {
if (availableApk.sig.equals(mostAppropriateSignature)) {
apk = availableApk;
break;
}
}
if (apk == null && apks.size() > 0) {
apk = apks.get(0);
}
}
return apk;
}
public static Apk findApkFromAnyRepo(Context context, String packageName, int versionCode) {
return findApkFromAnyRepo(context, packageName, versionCode, null, Cols.ALL);
return findApkFromAnyRepo(context, packageName, versionCode, null);
}
public static Apk findApkFromAnyRepo(Context context, String packageName, int versionCode,
String signature) {
return findApkFromAnyRepo(context, packageName, versionCode, signature, Cols.ALL);
}
public static Apk findApkFromAnyRepo(Context context, String packageName, int versionCode,
@Nullable String signature, String[] projection) {
final Uri uri = getApkFromAnyRepoUri(packageName, versionCode, signature);
return findByUri(context, uri, projection);
return findByUri(context, uri, Cols.ALL);
}
public static Apk findByUri(Context context, Uri uri, String[] projection) {

View File

@ -15,9 +15,12 @@ import android.text.TextUtils;
import android.util.Log;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Schema.AppMetadataTable;
import org.fdroid.fdroid.data.Schema.InstalledAppTable;
import org.fdroid.fdroid.data.Schema.InstalledAppTable.Cols;
import org.fdroid.fdroid.data.Schema.PackageTable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@ -28,17 +31,33 @@ public class InstalledAppProvider extends FDroidProvider {
public static class Helper {
public static App[] all(Context context) {
ArrayList<App> appList = new ArrayList<>();
Cursor cursor = context.getContentResolver().query(InstalledAppProvider.getAllAppsUri(),
null, null, null, null);
if (cursor != null) {
if (cursor.getCount() > 0) {
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
appList.add(new App(cursor));
cursor.moveToNext();
}
}
cursor.close();
}
return appList.toArray(new App[0]);
}
/**
* @return The keys are the package names, and their corresponding values are
* the {@link PackageInfo#lastUpdateTime last update time} in milliseconds.
*/
public static Map<String, Long> all(Context context) {
public static Map<String, Long> lastUpdateTimes(Context context) {
Map<String, Long> cachedInfo = new HashMap<>();
final Uri uri = InstalledAppProvider.getContentUri();
final String[] projection = Cols.ALL;
Cursor cursor = context.getContentResolver().query(uri, projection, null, null, null);
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
if (cursor != null) {
if (cursor.getCount() > 0) {
cursor.moveToFirst();
@ -58,7 +77,7 @@ public class InstalledAppProvider extends FDroidProvider {
@Nullable
public static InstalledApp findByPackageName(Context context, String packageName) {
Cursor cursor = context.getContentResolver().query(getAppUri(packageName), Cols.ALL, null, null, null);
Cursor cursor = context.getContentResolver().query(getAppUri(packageName), null, null, null, null);
if (cursor == null) {
return null;
}
@ -80,6 +99,8 @@ public class InstalledAppProvider extends FDroidProvider {
private static final String PATH_SEARCH = "search";
private static final int CODE_SEARCH = CODE_SINGLE + 1;
private static final String PATH_ALL_APPS = "allApps";
private static final int CODE_ALL_APPS = CODE_SEARCH + 1;
private static final UriMatcher MATCHER = new UriMatcher(-1);
@ -100,6 +121,7 @@ public class InstalledAppProvider extends FDroidProvider {
static {
MATCHER.addURI(getAuthority(), null, CODE_LIST);
MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*", CODE_SEARCH);
MATCHER.addURI(getAuthority(), PATH_ALL_APPS, CODE_ALL_APPS);
MATCHER.addURI(getAuthority(), "*", CODE_SINGLE);
}
@ -107,6 +129,10 @@ public class InstalledAppProvider extends FDroidProvider {
return Uri.parse("content://" + getAuthority());
}
public static Uri getAllAppsUri() {
return getContentUri().buildUpon().appendPath(PATH_ALL_APPS).build();
}
/**
* @return the {@link Uri} that points to a specific installed app
*/
@ -229,6 +255,7 @@ public class InstalledAppProvider extends FDroidProvider {
}
QuerySelection selection = new QuerySelection(customSelection, selectionArgs);
QueryBuilder query = null;
switch (MATCHER.match(uri)) {
case CODE_LIST:
selection = selectNotSystemSignature(selection);
@ -242,16 +269,31 @@ public class InstalledAppProvider extends FDroidProvider {
selection = selection.add(querySearch(uri.getLastPathSegment()));
break;
case CODE_ALL_APPS:
selection = selectNotSystemSignature(selection);
query = new QueryBuilder();
query.addField(Cols._ID);
query.appendField(Cols.APPLICATION_LABEL, null, Schema.AppMetadataTable.Cols.NAME);
query.appendField(Cols.VERSION_CODE, null, AppMetadataTable.Cols.UPSTREAM_VERSION_CODE);
query.appendField(Cols.VERSION_NAME, null, AppMetadataTable.Cols.UPSTREAM_VERSION_NAME);
query.appendField(PackageTable.Cols.PACKAGE_NAME, PackageTable.NAME,
AppMetadataTable.Cols.Package.PACKAGE_NAME);
break;
default:
String message = "Invalid URI for installed app content provider: " + uri;
Log.e(TAG, message);
throw new UnsupportedOperationException(message);
}
QueryBuilder query = new QueryBuilder();
query.addFields(projection);
if (projection.length == 0) {
query.addField(Cols._ID);
if (query != null) { // NOPMD
// the fields are already setup above
} else if (projection == null || projection.length == 0) {
query = new QueryBuilder();
query.addFields(Cols.ALL);
} else {
query = new QueryBuilder();
query.addFields(projection);
}
query.addSelection(selection);
query.addOrderBy(sortOrder);
@ -279,6 +321,12 @@ public class InstalledAppProvider extends FDroidProvider {
return count;
}
/**
* {@link Cols.Package#NAME} is not included in the database here, because
* it is included only in the {@link PackageTable}, since there are large
* cross-table queries needed to handle the complexity of multiple repos
* potentially serving the same apps.
*/
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {

View File

@ -176,7 +176,7 @@ public class InstalledAppProviderService extends JobIntentService {
*/
public static void compareToPackageManager(Context context) {
Utils.debugLog(TAG, "Comparing package manager to our installed app cache.");
Map<String, Long> cachedInfo = InstalledAppProvider.Helper.all(context);
Map<String, Long> cachedInfo = InstalledAppProvider.Helper.lastUpdateTimes(context);
List<PackageInfo> packageInfoList = context.getPackageManager()
.getInstalledPackages(PackageManager.GET_SIGNATURES);

View File

@ -7,6 +7,7 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.graphics.Outline;
import android.net.Uri;
import android.os.Build;
@ -22,15 +23,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 +46,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 +66,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 +101,9 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
@Nullable
private final Button secondaryButton;
@Nullable
private final CheckBox checkBox;
@Nullable
private App currentApp;
@ -107,6 +114,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 +155,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);
@ -176,7 +187,15 @@ public abstract class AppListItemController extends RecyclerView.ViewHolder {
public void bindModel(@NonNull App app) {
currentApp = app;
ImageLoader.getInstance().displayImage(app.iconUrl, icon, Utils.getRepoAppDisplayImageOptions());
if (app.iconUrl == null) {
try {
icon.setImageDrawable(activity.getPackageManager().getApplicationIcon(app.packageName));
} catch (PackageManager.NameNotFoundException e) {
// ignored
}
} else {
ImageLoader.getInstance().displayImage(app.iconUrl, icon, Utils.getRepoAppDisplayImageOptions());
}
// Figures out the current install/update/download/etc status for the app we are viewing.
// Then, asks the view to update itself to reflect this status.
@ -220,9 +239,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 +347,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 +564,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<String> wipeSet = prefs.getPanicTmpSelectedSet();
checkBox.toggle();
if (checkBox.isChecked()) {
wipeSet.add(currentApp.packageName);
} else {
wipeSet.remove(currentApp.packageName);
}
prefs.setPanicTmpSelectedSet(wipeSet);
}
};
}

View File

@ -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;
}
}

View File

@ -2,6 +2,7 @@ package org.fdroid.fdroid.views.installed;
import android.app.Activity;
import android.database.Cursor;
import android.provider.BaseColumns;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
@ -9,16 +10,15 @@ import android.view.View;
import android.view.ViewGroup;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Schema;
class InstalledAppListAdapter extends RecyclerView.Adapter<InstalledAppListItemController> {
public class InstalledAppListAdapter extends RecyclerView.Adapter<InstalledAppListItemController> {
private final Activity activity;
protected final Activity activity;
@Nullable
private Cursor cursor;
InstalledAppListAdapter(Activity activity) {
protected InstalledAppListAdapter(Activity activity) {
this.activity = activity;
setHasStableIds(true);
}
@ -30,7 +30,8 @@ class InstalledAppListAdapter extends RecyclerView.Adapter<InstalledAppListItemC
}
cursor.moveToPosition(position);
return cursor.getLong(cursor.getColumnIndex(Schema.AppMetadataTable.Cols.ROW_ID));
// TODO this should be based on Schema.InstalledAppProvider.Cols._ID
return cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
}
@NonNull

View File

@ -71,4 +71,14 @@
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" />
<CheckBox
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/icon"
android:clickable="false"
android:visibility="gone"/>
</android.support.constraint.ConstraintLayout>

View File

@ -206,6 +206,7 @@ This often occurs with apps installed via Google Play or other sources, if they
<string name="menu_install">Install</string>
<!-- This is a button label, it must be the right size, or the layout gets messed up. It should ideally be 10-20 characters. -->
<string name="menu_uninstall">Uninstall</string>
<string name="menu_select_for_wipe">Select for wipe</string>
<string name="menu_ignore_all">Ignore All Updates</string>
<string name="menu_ignore_this">Ignore This Update</string>
<string name="menu_website">Website</string>
@ -323,13 +324,18 @@ This often occurs with apps installed via Google Play or other sources, if they
<!-- "panic button" is a well known concept in English, it is good to find a similar metaphor in your language -->
<string name="panic_settings">Panic button settings</string>
<string name="panic_settings_summary">Actions to be taken in case of emergency</string>
<string name="panic_exit_title">Exit App</string>
<string name="panic_exit_summary">App will be closed </string>
<string name="panic_exit_title">Exit app</string>
<string name="panic_exit_summary">This app will be closed</string>
<string name="panic_destructive_actions">Destructive Actions</string>
<string name="panic_will_be_wiped">Will be uninstalled and all data deleted</string>
<string name="panic_apps_to_uninstall">Apps to be uninstalled and all data wiped</string>
<string name="panic_add_apps_to_uninstall">Add apps to be uninstalled and wiped</string>
<string name="panic_hide_title">Hide %s</string>
<string name="panic_hide_summary">App will hide itself</string>
<string name="panic_hide_warning_title">Remember how to restore</string>
<string name="panic_hide_warning_message">In a panic event, this will remove %1$s from the launcher. Only typing \"%2$d\" in the fake %3$s app can restore it.</string>
<string name="panic_reset_repos_title">Reset repos</string>
<string name="panic_reset_repos_summary">Force the repo setup back to defaults</string>
<!-- The name of a fake calculator app used as a disguise -->
<string name="hiding_calculator">Calculator</string>

View File

@ -392,23 +392,6 @@ public class ApkProviderTest extends FDroidProviderTest {
assertEquals("xxxxyyyy", apk.hash);
assertEquals("a hash type", apk.hashType);
String[] projection = {
Cols.Package.PACKAGE_NAME,
Cols.HASH,
};
Apk apkLessFields = ApkProvider.Helper.findApkFromAnyRepo(context, "com.example", 11, null, projection);
assertNotNull(apkLessFields);
assertEquals("com.example", apkLessFields.packageName);
assertEquals("xxxxyyyy", apkLessFields.hash);
// Didn't ask for these fields, so should be their default values...
assertNull(apkLessFields.hashType);
assertNull(apkLessFields.versionName);
assertEquals(0, apkLessFields.versionCode);
Apk notFound = ApkProvider.Helper.findApkFromAnyRepo(context, "com.doesnt.exist", 1000);
assertNull(notFound);
}

View File

@ -1,5 +1,6 @@
package org.fdroid.fdroid.data;
import android.content.ContentValues;
import android.content.ContextWrapper;
import org.fdroid.fdroid.TestUtils;
import org.junit.After;
@ -26,4 +27,10 @@ public abstract class FDroidProviderTest {
DBHelper.clearDbHelperSingleton();
}
protected Repo setEnabled(Repo repo, boolean enabled) {
ContentValues enable = new ContentValues(1);
enable.put(Schema.RepoTable.Cols.IN_USE, enabled);
RepoProvider.Helper.update(context, repo, enable);
return RepoProvider.Helper.findByAddress(context, repo.address);
}
}

View File

@ -20,6 +20,7 @@ import java.util.Map;
import static org.fdroid.fdroid.Assert.assertIsInstalledVersionInDb;
import static org.fdroid.fdroid.Assert.assertResultCount;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@ -35,7 +36,7 @@ public class InstalledAppProviderTest extends FDroidProviderTest {
@Test
public void insertSingleApp() {
Map<String, Long> foundBefore = InstalledAppProvider.Helper.all(RuntimeEnvironment.application);
Map<String, Long> foundBefore = InstalledAppProvider.Helper.lastUpdateTimes(RuntimeEnvironment.application);
assertEquals(foundBefore.size(), 0);
ContentValues values = new ContentValues();
@ -49,7 +50,7 @@ public class InstalledAppProviderTest extends FDroidProviderTest {
values.put(Cols.SIGNATURE, "000111222333444555666777888999aaabbbcccdddeeefff");
contentResolver.insert(InstalledAppProvider.getContentUri(), values);
Map<String, Long> foundAfter = InstalledAppProvider.Helper.all(RuntimeEnvironment.application);
Map<String, Long> foundAfter = InstalledAppProvider.Helper.lastUpdateTimes(RuntimeEnvironment.application);
assertEquals(1, foundAfter.size());
assertEquals(100000000L, foundAfter.get("org.example.test-app").longValue());
@ -71,6 +72,39 @@ public class InstalledAppProviderTest extends FDroidProviderTest {
cursor.close();
}
@Test
public void testHelperAll() {
final String packageName0 = "com.0";
final String packageName1 = "com.1";
final String packageName2 = "com.2";
App[] apps = InstalledAppProvider.Helper.all(context);
assertEquals(0, apps.length);
insertInstalledApp(packageName0, 0, "v0");
insertInstalledApp(packageName1, 1, "v1");
insertInstalledApp(packageName2, 2, "v2");
assertResultCount(contentResolver, 3, InstalledAppProvider.getContentUri());
assertResultCount(contentResolver, 3, InstalledAppProvider.getAllAppsUri());
assertIsInstalledVersionInDb(contentResolver, packageName0, 0, "v0");
assertIsInstalledVersionInDb(contentResolver, packageName1, 1, "v1");
assertIsInstalledVersionInDb(contentResolver, packageName2, 2, "v2");
apps = InstalledAppProvider.Helper.all(context);
assertEquals(3, apps.length);
assertEquals(packageName0, apps[0].packageName);
assertEquals("v0", apps[0].upstreamVersionName);
assertEquals(0, apps[0].upstreamVersionCode);
assertEquals(packageName1, apps[1].packageName);
assertEquals("v1", apps[1].upstreamVersionName);
assertEquals(1, apps[1].upstreamVersionCode);
assertEquals(packageName2, apps[2].packageName);
assertEquals("v2", apps[2].upstreamVersionName);
assertEquals(2, apps[2].upstreamVersionCode);
assertNotEquals(packageName0, apps[2].packageName);
}
@Test
public void testInsert() {
@ -84,6 +118,9 @@ public class InstalledAppProviderTest extends FDroidProviderTest {
assertIsInstalledVersionInDb(contentResolver, "com.example.com1", 1, "v1");
assertIsInstalledVersionInDb(contentResolver, "com.example.com2", 2, "v2");
assertIsInstalledVersionInDb(contentResolver, "com.example.com3", 3, "v3");
App[] apps = InstalledAppProvider.Helper.all(context);
assertEquals(3, apps.length);
}
@Test

View File

@ -77,13 +77,6 @@ public class RepoProviderTest extends FDroidProviderTest {
assertEquals(0, RepoProvider.Helper.countEnabledRepos(context));
}
private Repo setEnabled(Repo repo, boolean enabled) {
ContentValues enable = new ContentValues(1);
enable.put(RepoTable.Cols.IN_USE, enabled);
RepoProvider.Helper.update(context, repo, enable);
return RepoProvider.Helper.findByAddress(context, repo.address);
}
@Test
public void lastUpdated() {
assertNull(RepoProvider.Helper.lastUpdate(context));

View File

@ -1,4 +1,3 @@
package org.fdroid.fdroid.updater;
import android.content.ContentValues;

View File

@ -66,7 +66,7 @@ public class AppIconsTest extends MultiIndexUpdaterTest {
private void assertIconUrl(String expectedUrl) {
App app = AppProvider.Helper.findHighestPriorityMetadata(context.getContentResolver(),
"org.adaway", new String[] {Schema.AppMetadataTable.Cols.ICON_URL});
"org.adaway", new String[]{Schema.AppMetadataTable.Cols.ICON_URL});
assertEquals(app.iconUrl, expectedUrl);
}

View File

@ -1,4 +1,3 @@
package org.fdroid.fdroid.updater;
import org.fdroid.fdroid.BuildConfig;

View File

@ -1,6 +1,5 @@
package org.fdroid.fdroid.updater;
import android.content.ContentValues;
import org.fdroid.fdroid.BuildConfig;
import org.fdroid.fdroid.IndexUpdater;
import org.fdroid.fdroid.data.Apk;
@ -35,12 +34,6 @@ public class Issue763MultiRepo extends MultiIndexUpdaterTest {
antoxRepo = createRepo("Tox", "https://pkg.tox.chat/fdroid/repo", context, antoxCert);
}
public void setEnabled(Repo repo, boolean enabled) {
ContentValues values = new ContentValues(1);
values.put(Schema.RepoTable.Cols.IN_USE, enabled ? 1 : 0);
RepoProvider.Helper.update(context, repo, values);
}
@Test
public void antoxRepo() throws IndexUpdater.UpdateException {
assertAntoxEmpty();
@ -111,7 +104,7 @@ public class Issue763MultiRepo extends MultiIndexUpdaterTest {
private void assertAntoxExists() {
String packageName = "chat.tox.antox";
List<Apk> actualApksAfterUpdate = ApkProvider.Helper.findByRepo(context, antoxRepo, Schema.ApkTable.Cols.ALL);
int[] expectedVersions = new int[] {15421};
int[] expectedVersions = new int[]{15421};
assertApp(packageName, expectedVersions);
assertApksExist(actualApksAfterUpdate, packageName, expectedVersions);
@ -121,17 +114,17 @@ public class Issue763MultiRepo extends MultiIndexUpdaterTest {
List<Apk> actualApksAfterUpdate = ApkProvider.Helper.findByRepo(context, microGRepo, Schema.ApkTable.Cols.ALL);
String vendingPackage = "com.android.vending";
int[] expectedVendingVersions = new int[] {1};
int[] expectedVendingVersions = new int[]{1};
assertApp(vendingPackage, expectedVendingVersions);
assertApksExist(actualApksAfterUpdate, vendingPackage, expectedVendingVersions);
String gmsPackage = "com.google.android.gms";
int[] expectedGmsVersions = new int[] {11059462, 10545451, 10545440, 10087438, 10087435, 9258259, 8492252, };
int[] expectedGmsVersions = new int[]{11059462, 10545451, 10545440, 10087438, 10087435, 9258259, 8492252};
assertApp(gmsPackage, expectedGmsVersions);
assertApksExist(actualApksAfterUpdate, gmsPackage, expectedGmsVersions);
String gsfPackage = "com.google.android.gsf";
int[] expectedGsfVersions = new int[] {8};
int[] expectedGsfVersions = new int[]{8};
assertApp(gsfPackage, expectedGsfVersions);
assertApksExist(actualApksAfterUpdate, gsfPackage, expectedGsfVersions);
}

View File

@ -1,14 +1,13 @@
package org.fdroid.fdroid.updater;
import android.content.ContentValues;
import android.content.Context;
import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.fdroid.fdroid.IndexV1Updater;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.IndexUpdater;
import org.fdroid.fdroid.IndexUpdater.UpdateException;
import org.fdroid.fdroid.IndexV1Updater;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.TestUtils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;

View File

@ -1,4 +1,3 @@
package org.fdroid.fdroid.updater;
import android.content.ContentValues;
@ -41,7 +40,8 @@ public class ProperMultiIndexUpdaterTest extends MultiIndexUpdaterTest {
@Retention(RetentionPolicy.SOURCE)
@StringDef({"Conflicting", "Normal"})
public @interface RepoIdentifier { }
public @interface RepoIdentifier {
}
@Test
public void appsRemovedFromRepo() throws IndexUpdater.UpdateException {
@ -310,7 +310,9 @@ public class ProperMultiIndexUpdaterTest extends MultiIndexUpdaterTest {
assertAdAwayMetadata(adaway, id);
}
/** @see ProperMultiIndexUpdaterTest#assert2048Metadata(Repo, String) */
/**
* @see ProperMultiIndexUpdaterTest#assert2048Metadata(Repo, String)
*/
private void assertAdAwayMetadata(App adaway, @RepoIdentifier String id) {
assertNotNull(adaway);
assertEquals(String.format("AdAway", id),
@ -338,7 +340,9 @@ public class ProperMultiIndexUpdaterTest extends MultiIndexUpdaterTest {
assertAdbMetadata(adb, id);
}
/** @see ProperMultiIndexUpdaterTest#assert2048Metadata(Repo, String) */
/**
* @see ProperMultiIndexUpdaterTest#assert2048Metadata(Repo, String)
*/
private void assertAdbMetadata(App adb, @RepoIdentifier String id) {
assertNotNull(adb);
assertEquals("adbWireless", adb.name);
@ -355,7 +359,9 @@ public class ProperMultiIndexUpdaterTest extends MultiIndexUpdaterTest {
assertCalendarMetadata(calendar, id);
}
/** @see ProperMultiIndexUpdaterTest#assert2048Metadata(Repo, String) */
/**
* @see ProperMultiIndexUpdaterTest#assert2048Metadata(Repo, String)
*/
private void assertCalendarMetadata(App calendar, @RepoIdentifier String id) {
assertNotNull(calendar);
assertEquals("Add to calendar",

View File

@ -0,0 +1,53 @@
package org.fdroid.fdroid.views.panic;
import org.fdroid.fdroid.data.DBHelper;
import org.fdroid.fdroid.data.FDroidProviderTest;
import org.fdroid.fdroid.data.Repo;
import org.fdroid.fdroid.data.RepoProvider;
import org.fdroid.fdroid.data.RepoProviderTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import java.util.List;
import static org.junit.Assert.assertEquals;
@RunWith(RobolectricTestRunner.class)
public class PanicResponderActivityTest extends FDroidProviderTest {
/**
* The {@link DBHelper} class populates the default repos when it first creates a database.
* The names/URLs/signing certificates for these repos are all hard coded in the source/res.
*/
@Test
public void defaultRepos() {
List<Repo> defaultRepos = RepoProvider.Helper.all(context);
assertEquals(defaultRepos.size(), 4); // based on app/src/main/res/default_repo.xml
Repo gpRepo = RepoProvider.Helper.findByAddress(context, "https://guardianproject.info/fdroid/repo");
setEnabled(gpRepo, true);
assertEquals(2, RepoProvider.Helper.countEnabledRepos(context));
PanicResponderActivity.resetRepos(context);
assertEquals(1, RepoProvider.Helper.countEnabledRepos(context));
defaultRepos = RepoProvider.Helper.all(context);
assertEquals(4, defaultRepos.size());
RepoProviderTest.insertRepo(
context,
"https://mock-repo-1.example.com/fdroid/repo",
"Just a made up repo",
"ABCDEF1234567890",
"Mock Repo 1"
);
defaultRepos = RepoProvider.Helper.all(context);
assertEquals(5, defaultRepos.size());
assertEquals(2, RepoProvider.Helper.countEnabledRepos(context));
PanicResponderActivity.resetRepos(context);
defaultRepos = RepoProvider.Helper.all(context);
assertEquals(4, defaultRepos.size());
assertEquals(1, RepoProvider.Helper.countEnabledRepos(context));
}
}