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 947d86076286765d6c4d6dc4ad2cb6216d517e07..eba5d57eec64dfdc9645227d19e8809ecc793702 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 @@ -180,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 d9c5ac65eaee341ce3597704d26fdaf4f7dba433..fa9122c451fad394af99141c7b3716951b5897cd 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 @@ -2,6 +2,10 @@ 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; @@ -9,7 +13,10 @@ 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; @@ -24,11 +31,17 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; +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; @@ -38,7 +51,7 @@ 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()); @@ -65,17 +78,32 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor { 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<AppDetails> cachedApps = null; @Inject - ScreenFilterMonitorImpl(Application app, SharedPreferences prefs) { + ScreenFilterMonitorImpl(Application app, AndroidExecutor androidExecutor, + SharedPreferences prefs) { pm = app.getPackageManager(); + this.app = app; + this.androidExecutor = androidExecutor; this.prefs = prefs; } @Override @UiThread public Collection<AppDetails> getApps() { + if (cachedApps != null) return cachedApps; Set<String> allowed = prefs.getStringSet(PREF_KEY_ALLOWED, Collections.emptySet()); List<AppDetails> apps = new ArrayList<>(); @@ -89,11 +117,15 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor { } } Collections.sort(apps, (a, b) -> a.name.compareTo(b.name)); + apps = Collections.unmodifiableList(apps); + cachedApps = apps; return apps; } @Override + @UiThread public void allowApps(Collection<String> packageNames) { + cachedApps = null; Set<String> allowed = prefs.getStringSet(PREF_KEY_ALLOWED, Collections.emptySet()); Set<String> merged = new HashSet<>(allowed); @@ -121,12 +153,11 @@ class ScreenFilterMonitorImpl implements ScreenFilterMonitor { 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; + if (requestedPermissions[i].equals(SYSTEM_ALERT_WINDOW)) { + // 'flags' may be null on Robolectric + return flags == null || + (flags[i] & REQUESTED_PERMISSION_GRANTED) != 0; } } } else { @@ -163,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/BaseActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/BaseActivity.java index 59e0cca707012430a8b47a74361b933268b387b3..dd684b7d38f8fc025cde263b8eacc4885c112615 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 @@ -49,7 +49,10 @@ public abstract class BaseActivity extends AppCompatActivity private final List<ActivityLifecycleController> lifecycleControllers = new ArrayList<>(); private boolean destroyed = false; + private ScreenFilterDialogFragment dialogFrag; + private Toolbar toolbar = null; + private boolean searchedForToolbar = false; public abstract void injectActivity(ActivityComponent component); @@ -108,6 +111,12 @@ public abstract class BaseActivity extends AppCompatActivity } } + @Override + protected void onResume() { + super.onResume(); + protectToolbar(); + } + @Override protected void onPause() { super.onPause(); @@ -137,13 +146,18 @@ public abstract class BaseActivity extends AppCompatActivity // 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 there are no overlay apps that haven't been allowed, allow the tap if (apps.isEmpty()) return true; + // Create dialog 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()); + if (!fm.isStateSaved()) { + // When dialog is dismissed, update protection of toolbar + dialogFrag.setDismissListener(this::protectToolbar); + dialogFrag.show(fm, dialogFrag.getTag()); + } // Filter the tap return false; } @@ -199,16 +213,21 @@ 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) { - boolean filter = !screenFilterMonitor.getApps().isEmpty(); - toolbar.setFilterTouchesWhenObscured(filter); - } + findToolbar(); + if (toolbar != null) { + boolean filter = !screenFilterMonitor.getApps().isEmpty(); + toolbar.setFilterTouchesWhenObscured(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) { for (int i = 0, len = vg.getChildCount(); i < len; i++) { @@ -230,19 +249,16 @@ 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 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 037438b50d5462de0fb14dcb42edc5c49485f4eb..0539497c7101f5af898ac8afe44e414700dd692e 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 @@ -3,6 +3,7 @@ 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; @@ -32,6 +33,8 @@ public class ScreenFilterDialogFragment extends DialogFragment { @Inject ScreenFilterMonitor screenFilterMonitor; + DismissListener dismissListener = null; + public static ScreenFilterDialogFragment newInstance( Collection<AppDetails> apps) { ScreenFilterDialogFragment frag = new ScreenFilterDialogFragment(); @@ -46,6 +49,10 @@ public class ScreenFilterDialogFragment extends DialogFragment { return frag; } + public void setDismissListener(DismissListener dismissListener) { + this.dismissListener = dismissListener; + } + @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); @@ -83,4 +90,14 @@ public class ScreenFilterDialogFragment extends DialogFragment { }); return builder.create(); } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + if (dismissListener != null) dismissListener.onDialogDismissed(); + } + + public interface DismissListener { + void onDialogDismissed(); + } }