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 763c50332..cbd13b402 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 c0e068976..eba5d57ee 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() { @@ -174,8 +180,11 @@ public class AppModule { } @Provides + @Singleton ScreenFilterMonitor provideScreenFilterMonitor( + LifecycleManager lifecycleManager, ScreenFilterMonitorImpl screenFilterMonitor) { + lifecycleManager.registerService(screenFilterMonitor); return screenFilterMonitor; } 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 e1a88b592..fa9122c45 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,13 +1,22 @@ package org.briarproject.briar.android; +import android.annotation.SuppressLint; 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.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.Signature; import android.support.annotation.UiThread; +import org.briarproject.bramble.api.lifecycle.Service; +import org.briarproject.bramble.api.lifecycle.ServiceException; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.system.AndroidExecutor; import org.briarproject.bramble.util.StringUtils; import org.briarproject.briar.api.android.ScreenFilterMonitor; @@ -16,23 +25,33 @@ 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.concurrent.atomic.AtomicBoolean; 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.Intent.ACTION_PACKAGE_ADDED; +import static android.content.Intent.ACTION_PACKAGE_CHANGED; +import static android.content.Intent.ACTION_PACKAGE_REMOVED; +import static android.content.Intent.ACTION_PACKAGE_REPLACED; 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 -class ScreenFilterMonitorImpl implements ScreenFilterMonitor { +class ScreenFilterMonitorImpl implements ScreenFilterMonitor, Service { private static final Logger LOG = Logger.getLogger(ScreenFilterMonitorImpl.class.getName()); @@ -56,54 +75,93 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor { "82BA35E003C1B4B10DD244A8EE24FFFD333872AB5221985EDAB0FC0D" + "0B145B6AA192858E79020103"; + private static final String PREF_KEY_ALLOWED = "allowedOverlayApps"; + private final PackageManager pm; + private final Application app; + private final AndroidExecutor androidExecutor; + private final SharedPreferences prefs; + private final AtomicBoolean used = new AtomicBoolean(false); + + // UiThread + @Nullable + private BroadcastReceiver receiver = null; + + // UiThread + @Nullable + private Collection cachedApps = null; @Inject - ScreenFilterMonitorImpl(Application app) { + ScreenFilterMonitorImpl(Application app, AndroidExecutor androidExecutor, + SharedPreferences prefs) { pm = app.getPackageManager(); + this.app = app; + this.androidExecutor = androidExecutor; + this.prefs = prefs; } @Override @UiThread - public Set getApps() { - Set screenFilterApps = new TreeSet<>(); + public Collection getApps() { + if (cachedApps != null) return cachedApps; + Set allowed = prefs.getStringSet(PREF_KEY_ALLOWED, + Collections.emptySet()); + List apps = new ArrayList<>(); List 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)); + apps = Collections.unmodifiableList(apps); + cachedApps = apps; + return apps; } - // Fetches the application name for a given package. - @Nullable - private String pkgToString(PackageInfo pkgInfo) { + @Override + @UiThread + public void allowApps(Collection packageNames) { + cachedApps = null; + Set allowed = prefs.getStringSet(PREF_KEY_ALLOWED, + Collections.emptySet()); + Set 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; + for (int i = 0; i < requestedPermissions.length; i++) { + if (requestedPermissions[i].equals(SYSTEM_ALERT_WINDOW)) { + // 'flags' may be null on Robolectric + return flags == null || + (flags[i] & REQUESTED_PERMISSION_GRANTED) != 0; + } + } + } else { + // Check whether the permission has been requested for (String requestedPermission : requestedPermissions) { if (requestedPermission.equals(SYSTEM_ALERT_WINDOW)) { return true; @@ -113,6 +171,7 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor { return false; } + @SuppressLint("PackageManagerGetSignatures") private boolean isPlayServices(String pkg) { if (!PLAY_SERVICES_PACKAGE.equals(pkg)) return false; try { @@ -135,4 +194,36 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor { return false; } } + + @Override + public void startService() throws ServiceException { + if (used.getAndSet(true)) throw new IllegalStateException(); + androidExecutor.runOnUiThread(() -> { + IntentFilter filter = new IntentFilter(); + filter.addAction(ACTION_PACKAGE_ADDED); + filter.addAction(ACTION_PACKAGE_CHANGED); + filter.addAction(ACTION_PACKAGE_REMOVED); + filter.addAction(ACTION_PACKAGE_REPLACED); + filter.addDataScheme("package"); + receiver = new PackageBroadcastReceiver(); + app.registerReceiver(receiver, filter); + cachedApps = null; + }); + } + + @Override + public void stopService() throws ServiceException { + androidExecutor.runOnUiThread(() -> { + if (receiver != null) app.unregisterReceiver(receiver); + }); + } + + private class PackageBroadcastReceiver extends BroadcastReceiver { + + @Override + @UiThread + public void onReceive(Context context, Intent intent) { + cachedApps = null; + } + } } 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 f02774ae5..4066c384a 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 2a00be5cc..2651833f1 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 88e6302ec..7c8f5061a 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 @@ -4,6 +4,7 @@ import android.os.Bundle; import android.os.IBinder; import android.support.annotation.LayoutRes; import android.support.annotation.UiThread; +import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; @@ -21,13 +22,15 @@ import org.briarproject.briar.android.controller.ActivityLifecycleController; import org.briarproject.briar.android.forum.ForumModule; import org.briarproject.briar.android.fragment.BaseFragment; import org.briarproject.briar.android.fragment.ScreenFilterDialogFragment; +import org.briarproject.briar.android.util.UiUtils; 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; @@ -48,7 +51,10 @@ public abstract class BaseActivity extends AppCompatActivity private final List lifecycleControllers = new ArrayList<>(); private boolean destroyed = false; - private ScreenFilterDialogFragment dialogFrag; + + @Nullable + private Toolbar toolbar = null; + private boolean searchedForToolbar = false; public abstract void injectActivity(ActivityComponent component); @@ -57,8 +63,8 @@ public abstract class BaseActivity extends AppCompatActivity } @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + public void onCreate(@Nullable Bundle state) { + super.onCreate(state); if (PREVENT_SCREENSHOTS) getWindow().addFlags(FLAG_SECURE); @@ -97,6 +103,16 @@ public abstract class BaseActivity extends AppCompatActivity for (ActivityLifecycleController alc : lifecycleControllers) { alc.onActivityStart(); } + protectToolbar(); + ScreenFilterDialogFragment f = findDialogFragment(); + if (f != null) f.setDismissListener(this::protectToolbar); + } + + @Nullable + private ScreenFilterDialogFragment findDialogFragment() { + Fragment f = getSupportFragmentManager().findFragmentByTag( + ScreenFilterDialogFragment.TAG); + return (ScreenFilterDialogFragment) f; } @Override @@ -107,15 +123,6 @@ public abstract class BaseActivity extends AppCompatActivity } } - @Override - protected void onPause() { - super.onPause(); - if (dialogFrag != null) { - dialogFrag.dismiss(); - dialogFrag = null; - } - } - protected void showInitialFragment(BaseFragment f) { getSupportFragmentManager().beginTransaction() .replace(R.id.fragmentContainer, f, f.getUniqueTag()) @@ -132,16 +139,27 @@ public abstract class BaseActivity extends AppCompatActivity .commit(); } - private void showScreenFilterWarning() { - if (dialogFrag != null && dialogFrag.isVisible()) return; - Set apps = screenFilterMonitor.getApps(); - if (apps.isEmpty()) return; - dialogFrag = - ScreenFilterDialogFragment.newInstance(new ArrayList<>(apps)); - dialogFrag.setCancelable(false); + private boolean showScreenFilterWarning() { + // If the dialog is already visible, filter the tap + ScreenFilterDialogFragment f = findDialogFragment(); + if (f != null && f.isVisible()) return false; + Collection apps = screenFilterMonitor.getApps(); + // If all overlay apps have been allowed, allow the tap + if (apps.isEmpty()) return true; // Show dialog unless onSaveInstanceState() has been called, see #1112 FragmentManager fm = getSupportFragmentManager(); - if (!fm.isStateSaved()) dialogFrag.show(fm, dialogFrag.getTag()); + if (!fm.isStateSaved()) { + // Create dialog + f = ScreenFilterDialogFragment.newInstance(apps); + // When dialog is dismissed, update protection of toolbar + f.setDismissListener(this::protectToolbar); + // Hide soft keyboard when (re)showing dialog + View focus = getCurrentFocus(); + if (focus != null) hideSoftKeyboard(focus); + f.show(fm, ScreenFilterDialogFragment.TAG); + } + // Filter the tap + return false; } @Override @@ -195,15 +213,25 @@ public abstract class BaseActivity extends AppCompatActivity * is outside the wrapper. */ private void protectToolbar() { - View decorView = getWindow().getDecorView(); - if (decorView instanceof ViewGroup) { - Toolbar toolbar = findToolbar((ViewGroup) decorView); - if (toolbar != null) toolbar.setFilterTouchesWhenObscured(true); + findToolbar(); + if (toolbar != null) { + boolean filter = !screenFilterMonitor.getApps().isEmpty(); + UiUtils.setFilterTouchesWhenObscured(toolbar, filter); } } + private void findToolbar() { + if (searchedForToolbar) return; + View decorView = getWindow().getDecorView(); + if (decorView instanceof ViewGroup) + toolbar = findToolbar((ViewGroup) decorView); + searchedForToolbar = true; + } + @Nullable private Toolbar findToolbar(ViewGroup vg) { + // Views inside tap-safe layouts are already protected + if (vg instanceof TapSafeFrameLayout) return null; for (int i = 0, len = vg.getChildCount(); i < len; i++) { View child = vg.getChildAt(i); if (child instanceof Toolbar) return (Toolbar) child; @@ -223,23 +251,20 @@ public abstract class BaseActivity extends AppCompatActivity @Override public void setContentView(View v) { super.setContentView(makeTapSafeWrapper(v)); - protectToolbar(); } @Override public void setContentView(View v, LayoutParams layoutParams) { super.setContentView(makeTapSafeWrapper(v), layoutParams); - protectToolbar(); } @Override public void addContentView(View v, LayoutParams layoutParams) { super.addContentView(makeTapSafeWrapper(v), layoutParams); - protectToolbar(); } @Override - public void onTapFiltered() { - showScreenFilterWarning(); + public boolean shouldAllowTap() { + return showScreenFilterWarning(); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/BriarActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/BriarActivity.java index 37e8ed140..499696182 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/BriarActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/BriarActivity.java @@ -80,7 +80,7 @@ public abstract class BriarActivity extends BaseActivity { public void setSceneTransitionAnimation() { if (SDK_INT < 21) return; // workaround for #1007 - if (isSamsung7(this)) { + if (isSamsung7()) { return; } Transition slide = new Slide(Gravity.RIGHT); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java index 3126d8704..e579aea13 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/BlogPostViewHolder.java @@ -131,7 +131,7 @@ class BlogPostViewHolder extends RecyclerView.ViewHolder { i.putExtra(GROUP_ID, item.getGroupId().getBytes()); i.putExtra(POST_ID, item.getId().getBytes()); - if (Build.VERSION.SDK_INT >= 23 && !isSamsung7(ctx)) { + if (Build.VERSION.SDK_INT >= 23 && !isSamsung7()) { ActivityOptionsCompat options = makeSceneTransitionAnimation((Activity) ctx, layout, getTransitionName(item.getId())); 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 b1e644e96..b7c611665 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,106 @@ package org.briarproject.briar.android.fragment; +import android.annotation.SuppressLint; +import android.app.Activity; import android.app.Dialog; +import android.content.DialogInterface; 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 { + public static final String TAG = ScreenFilterDialogFragment.class.getName(); + + @Inject + ScreenFilterMonitor screenFilterMonitor; + + DismissListener dismissListener = null; + public static ScreenFilterDialogFragment newInstance( - ArrayList apps) { + Collection apps) { ScreenFilterDialogFragment frag = new ScreenFilterDialogFragment(); Bundle args = new Bundle(); - args.putStringArrayList("apps", apps); + ArrayList appNames = new ArrayList<>(); + for (AppDetails a : apps) appNames.add(a.name); + args.putStringArrayList("appNames", appNames); + ArrayList packageNames = new ArrayList<>(); + for (AppDetails a : apps) packageNames.add(a.packageName); + args.putStringArrayList("packageNames", packageNames); frag.setArguments(args); return frag; } + public void setDismissListener(DismissListener dismissListener) { + this.dismissListener = dismissListener; + } + + @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 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 appNames = args.getStringArrayList("appNames"); + ArrayList 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(); + }); + builder.setCancelable(false); return builder.create(); } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if (dismissListener != null) dismissListener.onDialogDismissed(); + } + + public interface DismissListener { + void onDialogDismissed(); + } } 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 7e31452fe..26e61f37f 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 @@ -6,7 +6,6 @@ import android.content.Context; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.os.PowerManager; import android.support.design.widget.TextInputLayout; import android.support.v4.app.FragmentManager; @@ -162,7 +161,7 @@ public class UiUtils { } public static boolean needsDozeWhitelisting(Context ctx) { - if (Build.VERSION.SDK_INT < 23) return false; + if (SDK_INT < 23) return false; PowerManager pm = (PowerManager) ctx.getSystemService(POWER_SERVICE); String packageName = ctx.getPackageName(); if (pm == null) throw new AssertionError(); @@ -178,8 +177,15 @@ public class UiUtils { return i; } - public static boolean isSamsung7(Context context) { + public static boolean isSamsung7() { return SDK_INT == 24 && MANUFACTURER.equalsIgnoreCase("Samsung"); } + public static void setFilterTouchesWhenObscured(View v, boolean filter) { + v.setFilterTouchesWhenObscured(filter); + // Workaround for Android bug #13530806, see + // https://android.googlesource.com/platform/frameworks/base/+/aba566589e0011c4b973c0d4f77be4e9ee176089%5E%21/core/java/android/view/View.java + if (v.getFilterTouchesWhenObscured() != filter) + v.setFilterTouchesWhenObscured(!filter); + } } 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 840e68b08..6d285682f 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 @@ -7,6 +7,7 @@ import android.view.MotionEvent; import android.widget.FrameLayout; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.android.util.UiUtils; import javax.annotation.Nullable; @@ -20,18 +21,18 @@ public class TapSafeFrameLayout extends FrameLayout { public TapSafeFrameLayout(Context context) { super(context); - setFilterTouchesWhenObscured(false); + UiUtils.setFilterTouchesWhenObscured(this, false); } public TapSafeFrameLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); - setFilterTouchesWhenObscured(false); + UiUtils.setFilterTouchesWhenObscured(this, false); } public TapSafeFrameLayout(Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); - setFilterTouchesWhenObscured(false); + UiUtils.setFilterTouchesWhenObscured(this, false); } public void setOnTapFilteredListener(OnTapFilteredListener listener) { @@ -40,12 +41,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 9df5add2f..a14b3b078 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 - Set getApps(); + Collection 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 + void allowApps(Collection 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 000000000..b883d16e7 --- /dev/null +++ b/briar-android/src/main/res/layout/dialog_screen_filter.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + \ 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 ce9467e21..776df599d 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -395,7 +395,8 @@ Screen overlay detected - 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 + 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\nThe following apps might be drawing on top:\n\n%1$s + Allow these apps to draw on top Camera permission