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 817bdd0e4e541c2a11851de0008acb7f28a85b5c..9923043643e8983be4adc12d72877df2e5337b7b 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 @@ -28,6 +28,7 @@ import org.briarproject.briar.BriarCoreModule; import org.briarproject.briar.android.reporting.BriarReportSender; import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.ReferenceManager; +import org.briarproject.briar.api.android.ScreenFilterMonitor; import org.briarproject.briar.api.blog.BlogManager; import org.briarproject.briar.api.blog.BlogPostFactory; import org.briarproject.briar.api.blog.BlogSharingManager; @@ -89,6 +90,8 @@ public interface AndroidComponent AndroidNotificationManager androidNotificationManager(); + ScreenFilterMonitor screenFilterMonitor(); + ConnectionRegistry connectionRegistry(); ContactManager contactManager(); 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 667b4ee2919447712ae626d9184d86ffcc90b8eb..479b3e94ca6d6bd3ce8fa8e6db9312eb86aa9f62 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 @@ -16,6 +16,7 @@ import org.briarproject.bramble.api.ui.UiCallback; import org.briarproject.bramble.util.StringUtils; import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.ReferenceManager; +import org.briarproject.briar.api.android.ScreenFilterMonitor; import java.io.File; import java.security.GeneralSecurityException; @@ -37,6 +38,8 @@ public class AppModule { static class EagerSingletons { @Inject AndroidNotificationManager androidNotificationManager; + @Inject + ScreenFilterMonitor screenFilterMonitor; } private final Application application; @@ -166,4 +169,12 @@ public class AppModule { eventBus.addListener(notificationManager); return notificationManager; } + + @Provides + @Singleton + ScreenFilterMonitor provideScreenFilterMonitor( + LifecycleManager lifecycleManager, ScreenFilterMonitorImpl sfm) { + lifecycleManager.registerService(sfm); + return sfm; + } } 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 new file mode 100644 index 0000000000000000000000000000000000000000..cb195108af4f941db686df4c512cb05cd6601b31 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/ScreenFilterMonitorImpl.java @@ -0,0 +1,232 @@ +package org.briarproject.briar.android; + +import android.app.Application; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v7.preference.PreferenceManager; + +import org.briarproject.bramble.api.lifecycle.Service; +import org.briarproject.bramble.api.lifecycle.ServiceException; +import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.system.AndroidExecutor; +import org.briarproject.briar.api.android.ScreenFilterMonitor; + +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static android.Manifest.permission.SYSTEM_ALERT_WINDOW; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class ScreenFilterMonitorImpl extends BroadcastReceiver + implements Service, + ScreenFilterMonitor { + + private static final Logger LOG = + Logger.getLogger(ScreenFilterMonitorImpl.class.getName()); + private static final String PREF_SCREEN_FILTER_APPS = + "shownScreenFilterApps"; + private final Context appContext; + private final AndroidExecutor androidExecutor; + private final LinkedList<String> appNames = new LinkedList<>(); + private final PackageManager pm; + private final SharedPreferences prefs; + private final AtomicBoolean used = new AtomicBoolean(false); + private final Set<String> apps = new HashSet<>(); + private final Set<String> shownApps; + // Used solely for the UiThread + private boolean serviceStarted = false; + + @Inject + ScreenFilterMonitorImpl(AndroidExecutor executor, Application app) { + this.androidExecutor = executor; + this.appContext = app; + pm = appContext.getPackageManager(); + prefs = PreferenceManager.getDefaultSharedPreferences(appContext); + shownApps = getShownScreenFilterApps(); + } + + @Override + public void startService() throws ServiceException { + if (used.getAndSet(true)) throw new IllegalStateException(); + Future<Void> f = androidExecutor.runOnUiThread(new Callable<Void>() { + @Override + public Void call() { + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(Intent.ACTION_PACKAGE_ADDED); + intentFilter.addDataScheme("package"); + appContext.registerReceiver(ScreenFilterMonitorImpl.this, + intentFilter); + apps.addAll(getInstalledScreenFilterApps()); + serviceStarted = true; + return null; + } + }); + try { + f.get(); + } catch (InterruptedException | ExecutionException e) { + throw new ServiceException(e); + } + } + + @Override + public void stopService() throws ServiceException { + Future<Void> f = androidExecutor.runOnUiThread(new Callable<Void>() { + @Override + public Void call() { + serviceStarted = false; + appContext.unregisterReceiver(ScreenFilterMonitorImpl.this); + return null; + } + }); + try { + f.get(); + } catch (InterruptedException | ExecutionException e) { + throw new ServiceException(e); + } + } + + private Set<String> getShownScreenFilterApps() { + // res must not be modified + Set<String> s = + prefs.getStringSet(PREF_SCREEN_FILTER_APPS, null); + HashSet<String> result = new HashSet<>(); + if (s != null) { + result.addAll(s); + } + return result; + } + + @Override + public void onReceive(Context context, Intent intent) { + if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) { + final String packageName = + intent.getData().getEncodedSchemeSpecificPart(); + androidExecutor.runOnUiThread(new Runnable() { + @Override + public void run() { + String pkg = isOverlayApp(packageName); + if (pkg == null) { + return; + } + apps.add(pkg); + } + }); + } + } + + @Override + @UiThread + public Set<String> getApps() { + if (!serviceStarted) { + apps.addAll(getInstalledScreenFilterApps()); + } + TreeSet<String> buf = new TreeSet<>(); + if (apps.isEmpty()) { + return buf; + } + buf.addAll(apps); + buf.removeAll(shownApps); + return buf; + } + + @Override + @UiThread + public void storeAppsAsShown(Collection<String> s, boolean persistent) { + HashSet<String> buf = new HashSet(s); + shownApps.addAll(buf); + if (persistent && !s.isEmpty()) { + buf.addAll(getShownScreenFilterApps()); + prefs.edit() + .putStringSet(PREF_SCREEN_FILTER_APPS, buf) + .apply(); + } + } + + private Set<String> getInstalledScreenFilterApps() { + HashSet<String> screenFilterApps = new HashSet<>(); + List<PackageInfo> packageInfos = + pm.getInstalledPackages(PackageManager.GET_PERMISSIONS); + for (PackageInfo packageInfo : packageInfos) { + if (isOverlayApp(packageInfo)) { + String name = pkgToString(packageInfo); + if (name != null) { + screenFilterApps.add(name); + } + } + } + return screenFilterApps; + } + + // Checks if pkg uses the SYSTEM_ALERT_WINDOW permission and if so + // returns the app name. + @Nullable + private String isOverlayApp(String pkg) { + try { + PackageInfo pkgInfo = + pm.getPackageInfo(pkg, PackageManager.GET_PERMISSIONS); + if (isOverlayApp(pkgInfo)) { + return pkgToString(pkgInfo); + } + } catch (PackageManager.NameNotFoundException ignored) { + if (LOG.isLoggable(Level.WARNING)) { + LOG.warning("Package name not found: " + pkg); + } + } + return null; + } + + // Fetch the application name for a given package. + @Nullable + private String pkgToString(PackageInfo pkgInfo) { + CharSequence seq = pm.getApplicationLabel(pkgInfo.applicationInfo); + if (seq != null) { + return seq.toString(); + } + return null; + } + + // Checks if an installed pkg is a user app using the permission. + private boolean isOverlayApp(PackageInfo packageInfo) { + int mask = ApplicationInfo.FLAG_SYSTEM | + ApplicationInfo.FLAG_UPDATED_SYSTEM_APP; + // Ignore system apps + if ((packageInfo.applicationInfo.flags & mask) != 0) { + return false; + } + //Get Permissions + String[] requestedPermissions = + packageInfo.requestedPermissions; + if (requestedPermissions != null) { + for (String requestedPermission : requestedPermissions) { + if (requestedPermission + .equals(SYSTEM_ALERT_WINDOW)) { + return true; + } + } + } + return false; + } +} 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 7c7b62c63c4cb0f969b8431f5665d0eb88587971..8b12f7e2b15cb7f6f4cc35bb7b0f923079b65bbc 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 @@ -13,11 +13,15 @@ import org.briarproject.briar.android.BriarApplication; import org.briarproject.briar.android.DestroyableContext; import org.briarproject.briar.android.controller.ActivityLifecycleController; import org.briarproject.briar.android.forum.ForumModule; +import org.briarproject.briar.android.fragment.SFDialogFragment; +import org.briarproject.briar.api.android.ScreenFilterMonitor; import java.util.ArrayList; import java.util.List; +import java.util.Set; import javax.annotation.Nullable; +import javax.inject.Inject; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT; @@ -25,13 +29,16 @@ import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOT public abstract class BaseActivity extends AppCompatActivity implements DestroyableContext { - protected ActivityComponent activityComponent; private final List<ActivityLifecycleController> lifecycleControllers = new ArrayList<>(); private boolean destroyed = false; + @Inject + protected ScreenFilterMonitor screenFilterMonitor; + private SFDialogFragment dialogFrag; + public abstract void injectActivity(ActivityComponent component); public void addLifecycleController(ActivityLifecycleController alc) { @@ -58,6 +65,7 @@ public abstract class BaseActivity extends AppCompatActivity for (ActivityLifecycleController alc : lifecycleControllers) { alc.onActivityCreate(this); } + } public ActivityComponent getActivityComponent() { @@ -89,6 +97,35 @@ public abstract class BaseActivity extends AppCompatActivity } } + @Override + protected void onPostResume() { + super.onPostResume(); + showNewScreenFilterWarning(); + } + + @Override + protected void onPause() { + super.onPause(); + if (dialogFrag != null) { + dialogFrag.dismiss(); + dialogFrag = null; + } + } + + protected void showNewScreenFilterWarning() { + final Set<String> apps = screenFilterMonitor.getApps(); + if (apps.isEmpty()) { + return; + } + dialogFrag = SFDialogFragment.newInstance(new ArrayList<>(apps)); + dialogFrag.setCancelable(false); + dialogFrag.show(getSupportFragmentManager(), "SFDialog"); + } + + public void rememberShownApps(ArrayList<String> s, boolean permanent) { + screenFilterMonitor.storeAppsAsShown(s, permanent); + } + @Override protected void onDestroy() { super.onDestroy(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/fragment/SFDialogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/fragment/SFDialogFragment.java new file mode 100644 index 0000000000000000000000000000000000000000..e20e814d685b249b2694aec1d85bf1ae6f33aa01 --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/fragment/SFDialogFragment.java @@ -0,0 +1,66 @@ +package org.briarproject.briar.android.fragment; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.Nullable; +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.MethodsNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.briar.R; +import org.briarproject.briar.android.activity.BaseActivity; + +import java.util.ArrayList; + +@MethodsNotNullByDefault +@ParametersNotNullByDefault +public class SFDialogFragment extends DialogFragment { + + public static SFDialogFragment newInstance(ArrayList<String> apps) { + SFDialogFragment frag = new SFDialogFragment(); + Bundle args = new Bundle(); + args.putStringArrayList("apps", apps); + frag.setArguments(args); + return frag; + } + + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + AlertDialog.Builder builder = + new AlertDialog.Builder( + getActivity(), + R.style.BriarDialogThemeNoFilter); + builder.setTitle(R.string.screen_filter_title); + LayoutInflater li = getActivity().getLayoutInflater(); + //Pass null here because it's an AlertDialog + View v = + li.inflate(R.layout.alert_dialog_checkbox, null, + false); + TextView t = (TextView) v.findViewById(R.id.alert_dialog_text); + final ArrayList<String> apps = + getArguments().getStringArrayList("apps"); + t.setText(getString(R.string.screen_filter_body, TextUtils + .join("\n", apps))); + final CheckBox cb = + (CheckBox) v.findViewById( + R.id.checkBox_screen_filter_reminder); + builder.setNeutralButton(R.string.continue_button, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, + int which) { + ((BaseActivity) getActivity()) + .rememberShownApps(apps, cb.isChecked()); + } + }); + builder.setView(v); + return builder.create(); + } +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/splash/SplashScreenActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/splash/SplashScreenActivity.java index 283ea0feb6b54b1cbd4591d9cb8ab0331ba58921..21c2e0ede23e1f35689df3a9d6e837d48c87e975 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/splash/SplashScreenActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/splash/SplashScreenActivity.java @@ -83,6 +83,10 @@ public class SplashScreenActivity extends BaseActivity { } } + @Override + protected void showNewScreenFilterWarning() { + } + private void enableStrictMode() { if (TESTING) { ThreadPolicy.Builder threadPolicy = new ThreadPolicy.Builder(); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java index 7d2e898eb0ad187481bb9dfd70595fd15cde82ee..d6346fffe29ffc013f23d37d92d4f0e06b604cdf 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/util/UiUtils.java @@ -120,5 +120,5 @@ public class UiUtils { public static String getBulbTransitionName(ContactId c) { return "bulb" + c.getInt(); } - + } 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 new file mode 100644 index 0000000000000000000000000000000000000000..b596ecbb2ad3c2ff310cd766b5472430209d60fb --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/api/android/ScreenFilterMonitor.java @@ -0,0 +1,15 @@ +package org.briarproject.briar.api.android; + +import android.support.annotation.UiThread; + +import java.util.Collection; +import java.util.Set; + +public interface ScreenFilterMonitor { + + @UiThread + Set<String> getApps(); + + @UiThread + void storeAppsAsShown(Collection<String> s, boolean persistent); +} diff --git a/briar-android/src/main/res/layout/alert_dialog_checkbox.xml b/briar-android/src/main/res/layout/alert_dialog_checkbox.xml new file mode 100644 index 0000000000000000000000000000000000000000..718589da9a6be8aca0a35609fd29c591bf5634de --- /dev/null +++ b/briar-android/src/main/res/layout/alert_dialog_checkbox.xml @@ -0,0 +1,40 @@ +<?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:filterTouchesWhenObscured="false" + android:orientation="vertical"> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:fadeScrollbars="false" + android:paddingLeft="20dp" + android:paddingRight="20dp" + android:paddingStart="20dp" + android:paddingTop="20dp" + android:theme="@style/BriarTheme"> + + <TextView + android:id="@+id/alert_dialog_text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="3dp" + android:paddingRight="6dp" + android:text="TextView" + android:textAppearance="@style/BriarTextBody" + android:theme="@+theme/BriarDialogTheme"/> + </ScrollView> + + <CheckBox + android:id="@+id/checkBox_screen_filter_reminder" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginLeft="15dp" + android:layout_marginStart="15dp" + android:layout_weight="0" + android:filterTouchesWhenObscured="false" + android:text="@string/checkbox_dont_show_again" + android:textAppearance="@style/BriarTextBody"/> +</LinearLayout> diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 4cdd0efb7cdc674d57c63bb7551776c40c083390..a10f34cafa8da8473417623149a8e0cfa33d8a94 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -380,4 +380,10 @@ <!-- Sign Out --> <string name="progress_title_logout">Signing out of Briar…</string> + <!-- Screen Filters & Tapjacking --> + <string name="screen_filter_title">Screen filter detected</string> + <string name="screen_filter_body">The following apps have permission to draw over other apps:\n\n%1$s \n\nBriar will not respond to touches when another app is drawing over it. +If you experience any problems, try turning off these apps when using Briar.\n</string> + <string name="checkbox_dont_show_again">Don\'t warn me again for these apps</string> + </resources> diff --git a/briar-android/src/main/res/values/themes.xml b/briar-android/src/main/res/values/themes.xml index 1676cf7dfcd1d0e28ecc400893727e1c322ef132..db57d78d40b380d4177193210e72b52abfb71f3c 100644 --- a/briar-android/src/main/res/values/themes.xml +++ b/briar-android/src/main/res/values/themes.xml @@ -14,6 +14,7 @@ <item name="android:textColorTertiaryInverse">@color/briar_text_tertiary_inverse</item> <item name="android:textColorLink">@color/briar_text_link</item> <item name="android:windowAnimationStyle">@style/ActivityAnimation</item> + <item name="android:filterTouchesWhenObscured">true</item> <!-- These fix a long-standing UI bug in the support preference library --> <item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item> @@ -47,6 +48,12 @@ <item name="android:textColorTertiaryInverse">@color/briar_text_tertiary_inverse</item> <item name="android:textColorLink">@color/briar_text_link</item> <item name="android:windowAnimationStyle">@style/DialogAnimation</item> + <item name="android:filterTouchesWhenObscured">true</item> + </style> + + <!-- Use this with care. Only used for the screen filter warning dialog --> + <style name="BriarDialogThemeNoFilter" parent="BriarDialogTheme"> + <item name="android:filterTouchesWhenObscured">false</item> </style> <style name="DialogAnimation" parent="@android:style/Animation.Dialog">