diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java index 1c6cc9d86f359dc248f9c30f54198fc4274d94c6..02a5dc0ff71fad378ff9ecc856fee8d87e304b33 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java @@ -1,5 +1,7 @@ package org.briarproject.briar.android; +import android.content.SharedPreferences; + import org.briarproject.bramble.BrambleAndroidModule; import org.briarproject.bramble.BrambleCoreEagerSingletons; import org.briarproject.bramble.BrambleCoreModule; @@ -89,6 +91,8 @@ public interface AndroidComponent AndroidNotificationManager androidNotificationManager(); + SharedPreferences sharedPreferences(); + ScreenFilterMonitor screenFilterMonitor(); ConnectionRegistry connectionRegistry(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java index c0e06897655f5c442ed6473a00814ad2e8673533..947d86076286765d6c4d6dc4ad2cb6216d517e07 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/AppModule.java @@ -1,6 +1,7 @@ package org.briarproject.briar.android; import android.app.Application; +import android.content.SharedPreferences; import org.briarproject.bramble.api.crypto.CryptoComponent; import org.briarproject.bramble.api.crypto.PublicKey; @@ -157,6 +158,11 @@ public class AppModule { return devConfig; } + @Provides + SharedPreferences provideSharedPreferences(Application app) { + return app.getSharedPreferences("db", MODE_PRIVATE); + } + @Provides @Singleton ReferenceManager provideReferenceManager() { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/ScreenFilterMonitorImpl.java b/briar-android/src/main/java/org/briarproject/briar/android/ScreenFilterMonitorImpl.java index e1a88b592841989d0f536b3c6601f2fe21d5088d..d9c5ac65eaee341ce3597704d26fdaf4f7dba433 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/ScreenFilterMonitorImpl.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/ScreenFilterMonitorImpl.java @@ -1,6 +1,8 @@ package org.briarproject.briar.android; +import android.annotation.SuppressLint; import android.app.Application; +import android.content.SharedPreferences; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; @@ -16,19 +18,23 @@ import java.io.InputStream; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.TreeSet; import java.util.logging.Logger; -import javax.annotation.Nullable; import javax.inject.Inject; import static android.Manifest.permission.SYSTEM_ALERT_WINDOW; import static android.content.pm.ApplicationInfo.FLAG_SYSTEM; import static android.content.pm.ApplicationInfo.FLAG_UPDATED_SYSTEM_APP; +import static android.content.pm.PackageInfo.REQUESTED_PERMISSION_GRANTED; import static android.content.pm.PackageManager.GET_PERMISSIONS; import static android.content.pm.PackageManager.GET_SIGNATURES; +import static android.os.Build.VERSION.SDK_INT; import static java.util.logging.Level.WARNING; @NotNullByDefault @@ -56,54 +62,75 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor { "82BA35E003C1B4B10DD244A8EE24FFFD333872AB5221985EDAB0FC0D" + "0B145B6AA192858E79020103"; + private static final String PREF_KEY_ALLOWED = "allowedOverlayApps"; + private final PackageManager pm; + private final SharedPreferences prefs; @Inject - ScreenFilterMonitorImpl(Application app) { + ScreenFilterMonitorImpl(Application app, SharedPreferences prefs) { pm = app.getPackageManager(); + this.prefs = prefs; } @Override @UiThread - public Set<String> getApps() { - Set<String> screenFilterApps = new TreeSet<>(); + public Collection<AppDetails> getApps() { + Set<String> allowed = prefs.getStringSet(PREF_KEY_ALLOWED, + Collections.emptySet()); + List<AppDetails> apps = new ArrayList<>(); List<PackageInfo> packageInfos = pm.getInstalledPackages(GET_PERMISSIONS); for (PackageInfo packageInfo : packageInfos) { - if (isOverlayApp(packageInfo)) { - String name = pkgToString(packageInfo); - if (name != null) { - screenFilterApps.add(name); - } + if (!allowed.contains(packageInfo.packageName) + && isOverlayApp(packageInfo)) { + String name = getAppName(packageInfo); + apps.add(new AppDetails(name, packageInfo.packageName)); } } - return screenFilterApps; + Collections.sort(apps, (a, b) -> a.name.compareTo(b.name)); + return apps; } - // Fetches the application name for a given package. - @Nullable - private String pkgToString(PackageInfo pkgInfo) { + @Override + public void allowApps(Collection<String> packageNames) { + Set<String> allowed = prefs.getStringSet(PREF_KEY_ALLOWED, + Collections.emptySet()); + Set<String> merged = new HashSet<>(allowed); + merged.addAll(packageNames); + prefs.edit().putStringSet(PREF_KEY_ALLOWED, merged).apply(); + } + + // Returns the application name for a given package, or the package name + // if no application name is available + private String getAppName(PackageInfo pkgInfo) { CharSequence seq = pm.getApplicationLabel(pkgInfo.applicationInfo); - if (seq != null) { - return seq.toString(); - } - return null; + return seq == null ? pkgInfo.packageName : seq.toString(); } // Checks if an installed package is a user app using the permission. private boolean isOverlayApp(PackageInfo packageInfo) { int mask = FLAG_SYSTEM | FLAG_UPDATED_SYSTEM_APP; // Ignore system apps - if ((packageInfo.applicationInfo.flags & mask) != 0) { - return false; - } + if ((packageInfo.applicationInfo.flags & mask) != 0) return false; // Ignore Play Services, it's effectively a system app - if (isPlayServices(packageInfo.packageName)) { - return false; - } + if (isPlayServices(packageInfo.packageName)) return false; // Get permissions String[] requestedPermissions = packageInfo.requestedPermissions; - if (requestedPermissions != null) { + if (requestedPermissions == null) return false; + if (SDK_INT >= 16 && SDK_INT < 23) { + // Check whether the permission has been requested and granted + int[] flags = packageInfo.requestedPermissionsFlags; + if (flags == null || flags.length != requestedPermissions.length) + throw new AssertionError(); + for (int i = 0; i < requestedPermissions.length; i++) { + if (requestedPermissions[i].equals(SYSTEM_ALERT_WINDOW) + && (flags[i] & REQUESTED_PERMISSION_GRANTED) != 0) { + return true; + } + } + } else { + // Check whether the permission has been requested for (String requestedPermission : requestedPermissions) { if (requestedPermission.equals(SYSTEM_ALERT_WINDOW)) { return true; @@ -113,6 +140,7 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor { return false; } + @SuppressLint("PackageManagerGetSignatures") private boolean isPlayServices(String pkg) { if (!PLAY_SERVICES_PACKAGE.equals(pkg)) return false; try { diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java index f02774ae517e3669d75d62c1e2b3d9e5a494bf7a..4066c384a7fe756d0546e654dea7a7113e753b84 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityComponent.java @@ -21,6 +21,7 @@ import org.briarproject.briar.android.forum.CreateForumActivity; import org.briarproject.briar.android.forum.ForumActivity; import org.briarproject.briar.android.forum.ForumListFragment; import org.briarproject.briar.android.forum.ForumModule; +import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment; import org.briarproject.briar.android.introduction.ContactChooserFragment; import org.briarproject.briar.android.introduction.IntroductionActivity; import org.briarproject.briar.android.introduction.IntroductionMessageFragment; @@ -152,7 +153,9 @@ public interface ActivityComponent { // Fragments void inject(AuthorNameFragment fragment); + void inject(PasswordFragment fragment); + void inject(DozeFragment fragment); void inject(ContactListFragment fragment); @@ -189,4 +192,5 @@ public interface ActivityComponent { void inject(SettingsFragment fragment); + void inject(ScreenFilterDialogFragment fragment); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityModule.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityModule.java index 2a00be5ccfd4d5eb1a0810adf73945933091e61a..2651833f194b065326a5e063c17e2f0bc3ac2285 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityModule.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/ActivityModule.java @@ -1,7 +1,6 @@ package org.briarproject.briar.android.activity; import android.app.Activity; -import android.content.SharedPreferences; import org.briarproject.briar.android.controller.BriarController; import org.briarproject.briar.android.controller.BriarControllerImpl; @@ -19,7 +18,6 @@ import org.briarproject.briar.android.navdrawer.NavDrawerControllerImpl; import dagger.Module; import dagger.Provides; -import static android.content.Context.MODE_PRIVATE; import static org.briarproject.briar.android.BriarService.BriarServiceConnection; @Module @@ -57,12 +55,6 @@ public class ActivityModule { return configController; } - @ActivityScope - @Provides - SharedPreferences provideSharedPreferences(Activity activity) { - return activity.getSharedPreferences("db", MODE_PRIVATE); - } - @ActivityScope @Provides PasswordController providePasswordController( diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java index 88e6302ecb7fb01ae19de8dc65f1a0737483da0b..59e0cca707012430a8b47a74361b933268b387b3 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java @@ -24,10 +24,11 @@ import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment; import org.briarproject.briar.android.widget.TapSafeFrameLayout; import org.briarproject.briar.android.widget.TapSafeFrameLayout.OnTapFilteredListener; import org.briarproject.briar.api.android.ScreenFilterMonitor; +import org.briarproject.briar.api.android.ScreenFilterMonitor.AppDetails; import java.util.ArrayList; +import java.util.Collection; import java.util.List; -import java.util.Set; import javax.annotation.Nullable; import javax.inject.Inject; @@ -132,16 +133,19 @@ public abstract class BaseActivity extends AppCompatActivity .commit(); } - private void showScreenFilterWarning() { - if (dialogFrag != null && dialogFrag.isVisible()) return; - Set<String> apps = screenFilterMonitor.getApps(); - if (apps.isEmpty()) return; - dialogFrag = - ScreenFilterDialogFragment.newInstance(new ArrayList<>(apps)); + private boolean showScreenFilterWarning() { + // If the dialog is already visible, filter the tap + if (dialogFrag != null && dialogFrag.isVisible()) return false; + Collection<AppDetails> apps = screenFilterMonitor.getApps(); + // If all overlay apps are allowed or system apps, allow the tap + if (apps.isEmpty()) return true; + dialogFrag = ScreenFilterDialogFragment.newInstance(apps); dialogFrag.setCancelable(false); // Show dialog unless onSaveInstanceState() has been called, see #1112 FragmentManager fm = getSupportFragmentManager(); if (!fm.isStateSaved()) dialogFrag.show(fm, dialogFrag.getTag()); + // Filter the tap + return false; } @Override @@ -198,7 +202,10 @@ public abstract class BaseActivity extends AppCompatActivity View decorView = getWindow().getDecorView(); if (decorView instanceof ViewGroup) { Toolbar toolbar = findToolbar((ViewGroup) decorView); - if (toolbar != null) toolbar.setFilterTouchesWhenObscured(true); + if (toolbar != null) { + boolean filter = !screenFilterMonitor.getApps().isEmpty(); + toolbar.setFilterTouchesWhenObscured(filter); + } } } @@ -239,7 +246,7 @@ public abstract class BaseActivity extends AppCompatActivity } @Override - public void onTapFiltered() { - showScreenFilterWarning(); + public boolean shouldAllowTap() { + return showScreenFilterWarning(); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/fragment/ScreenFilterDialogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/fragment/ScreenFilterDialogFragment.java index b1e644e96a35c4cee93f321abd32f3ffcb032474..037438b50d5462de0fb14dcb42edc5c49485f4eb 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/fragment/ScreenFilterDialogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/fragment/ScreenFilterDialogFragment.java @@ -1,40 +1,86 @@ package org.briarproject.briar.android.fragment; +import android.annotation.SuppressLint; +import android.app.Activity; import android.app.Dialog; import android.os.Bundle; import android.support.v4.app.DialogFragment; import android.support.v7.app.AlertDialog; import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.CheckBox; +import android.widget.TextView; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.BaseActivity; +import org.briarproject.briar.api.android.ScreenFilterMonitor; +import org.briarproject.briar.api.android.ScreenFilterMonitor.AppDetails; import java.util.ArrayList; +import java.util.Collection; import javax.annotation.Nullable; +import javax.inject.Inject; -@NotNullByDefault +@MethodsNotNullByDefault +@ParametersNotNullByDefault public class ScreenFilterDialogFragment extends DialogFragment { + @Inject + ScreenFilterMonitor screenFilterMonitor; + public static ScreenFilterDialogFragment newInstance( - ArrayList<String> apps) { + Collection<AppDetails> apps) { ScreenFilterDialogFragment frag = new ScreenFilterDialogFragment(); Bundle args = new Bundle(); - args.putStringArrayList("apps", apps); + ArrayList<String> appNames = new ArrayList<>(); + for (AppDetails a : apps) appNames.add(a.name); + args.putStringArrayList("appNames", appNames); + ArrayList<String> packageNames = new ArrayList<>(); + for (AppDetails a : apps) packageNames.add(a.packageName); + args.putStringArrayList("packageNames", packageNames); frag.setArguments(args); return frag; } + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + Activity activity = getActivity(); + if (activity == null) throw new IllegalStateException(); + ((BaseActivity) activity).getActivityComponent().inject(this); + } + @Override public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity(), + Activity activity = getActivity(); + if (activity == null) throw new IllegalStateException(); + AlertDialog.Builder builder = new AlertDialog.Builder(activity, R.style.BriarDialogThemeNoFilter); builder.setTitle(R.string.screen_filter_title); - ArrayList<String> apps = getArguments().getStringArrayList("apps"); - builder.setMessage(getString(R.string.screen_filter_body, - TextUtils.join("\n", apps))); - builder.setNeutralButton(R.string.continue_button, - (dialog, which) -> dialog.dismiss()); + Bundle args = getArguments(); + if (args == null) throw new IllegalStateException(); + ArrayList<String> appNames = args.getStringArrayList("appNames"); + ArrayList<String> packageNames = + args.getStringArrayList("packageNames"); + if (appNames == null || packageNames == null) + throw new IllegalStateException(); + LayoutInflater inflater = activity.getLayoutInflater(); + // See https://stackoverflow.com/a/24720976/6314875 + @SuppressLint("InflateParams") + View dialogView = inflater.inflate(R.layout.dialog_screen_filter, null); + builder.setView(dialogView); + TextView message = dialogView.findViewById(R.id.screen_filter_message); + message.setText(getString(R.string.screen_filter_body, + TextUtils.join("\n", appNames))); + CheckBox allow = dialogView.findViewById(R.id.screen_filter_checkbox); + builder.setNeutralButton(R.string.continue_button, (dialog, which) -> { + if (allow.isChecked()) screenFilterMonitor.allowApps(packageNames); + dialog.dismiss(); + }); return builder.create(); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/widget/TapSafeFrameLayout.java b/briar-android/src/main/java/org/briarproject/briar/android/widget/TapSafeFrameLayout.java index 840e68b0852e6f3ff5f4aaf372d31bfa77ef1068..701406e1fa1f838cd6bbea3f01cbdab0bd1738dd 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/widget/TapSafeFrameLayout.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/widget/TapSafeFrameLayout.java @@ -40,12 +40,12 @@ public class TapSafeFrameLayout extends FrameLayout { @Override public boolean onFilterTouchEventForSecurity(MotionEvent e) { - boolean filter = (e.getFlags() & FLAG_WINDOW_IS_OBSCURED) != 0; - if (filter && listener != null) listener.onTapFiltered(); - return !filter; + boolean obscured = (e.getFlags() & FLAG_WINDOW_IS_OBSCURED) != 0; + if (obscured && listener != null) return listener.shouldAllowTap(); + else return !obscured; } public interface OnTapFilteredListener { - void onTapFiltered(); + boolean shouldAllowTap(); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/api/android/ScreenFilterMonitor.java b/briar-android/src/main/java/org/briarproject/briar/api/android/ScreenFilterMonitor.java index 9df5add2f704673a7000300c039d8e2bcfdf3616..a14b3b078b5bf814dbaec6ff7d26c5482282a0f8 100644 --- a/briar-android/src/main/java/org/briarproject/briar/api/android/ScreenFilterMonitor.java +++ b/briar-android/src/main/java/org/briarproject/briar/api/android/ScreenFilterMonitor.java @@ -2,10 +2,37 @@ package org.briarproject.briar.api.android; import android.support.annotation.UiThread; -import java.util.Set; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import java.util.Collection; + +@NotNullByDefault public interface ScreenFilterMonitor { + /** + * Returns the details of all apps that have requested the + * SYSTEM_ALERT_WINDOW permission, excluding system apps, Google Play + * Services, and any apps that have been allowed by calling + * {@link #allowApps(Collection)}. + */ + @UiThread + Collection<AppDetails> getApps(); + + /** + * Allows the apps with the given package names to use overlay windows. + * They will not be returned by future calls to {@link #getApps()}. + */ @UiThread - Set<String> getApps(); + void allowApps(Collection<String> packageNames); + + class AppDetails { + + public final String name; + public final String packageName; + + public AppDetails(String name, String packageName) { + this.name = name; + this.packageName = packageName; + } + } } diff --git a/briar-android/src/main/res/layout/dialog_screen_filter.xml b/briar-android/src/main/res/layout/dialog_screen_filter.xml new file mode 100644 index 0000000000000000000000000000000000000000..c09d0dea7950c6e42ba23a6771b250b2a113c1f7 --- /dev/null +++ b/briar-android/src/main/res/layout/dialog_screen_filter.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:padding="@dimen/margin_large"> + + <ScrollView + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/screen_filter_message" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + + </ScrollView> + + <CheckBox + android:id="@+id/screen_filter_checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/margin_large" + android:text="@string/screen_filter_allow"/> + +</LinearLayout> \ No newline at end of file diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 97da612ac0fd9f2437bde989bfae6ba8c1834c1f..7f27eeada18f487a73cb760f1e07702a5d2ff8e5 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -397,6 +397,7 @@ <!-- Screen Filters & Tapjacking --> <string name="screen_filter_title">Screen overlay detected</string> <string name="screen_filter_body">Another app is drawing on top of Briar. To protect your security, Briar will not respond to touches when another app is drawing on top.\n\nTry turning off the following apps when using Briar:\n\n%1$s</string> + <string name="screen_filter_allow">Allow these apps to draw on top</string> <!-- Permission Requests --> <string name="permission_camera_title">Camera permission</string>