diff --git a/mobile/android/base/java/org/mozilla/gecko/customtabs/ActionBarPresenter.java b/mobile/android/base/java/org/mozilla/gecko/customtabs/ActionBarPresenter.java index 4ba49e96373f..3ae139ac364a 100644 --- a/mobile/android/base/java/org/mozilla/gecko/customtabs/ActionBarPresenter.java +++ b/mobile/android/base/java/org/mozilla/gecko/customtabs/ActionBarPresenter.java @@ -6,8 +6,6 @@ package org.mozilla.gecko.customtabs; import android.content.res.Resources; -import android.content.res.TypedArray; -import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; @@ -227,12 +225,12 @@ public class ActionBarPresenter { if (identity == null) { mIconView.setVisibility(View.INVISIBLE); } else { - final SecurityModeUtil.Mode mode = SecurityModeUtil.resolve(identity); + final SecurityModeUtil.IconType type = SecurityModeUtil.resolve(identity); mIconView.setVisibility(View.VISIBLE); - mIconView.setImageLevel(mode.ordinal()); + mIconView.setImageLevel(SecurityModeUtil.getImageLevel(type)); mIdentityPopup.setSiteIdentity(identity); - if (mode == SecurityModeUtil.Mode.LOCK_SECURE) { + if (type == SecurityModeUtil.IconType.LOCK_SECURE) { // Lock-Secure is special case. Keep its original green color. DrawableCompat.setTintList(mIconView.getDrawable(), null); } else { diff --git a/mobile/android/base/java/org/mozilla/gecko/toolbar/SecurityModeUtil.java b/mobile/android/base/java/org/mozilla/gecko/toolbar/SecurityModeUtil.java index f757a04baf1c..30e54b5a2750 100644 --- a/mobile/android/base/java/org/mozilla/gecko/toolbar/SecurityModeUtil.java +++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/SecurityModeUtil.java @@ -5,8 +5,11 @@ package org.mozilla.gecko.toolbar; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import android.text.TextUtils; +import org.mozilla.gecko.AboutPages; import org.mozilla.gecko.SiteIdentity; import org.mozilla.gecko.SiteIdentity.MixedMode; import org.mozilla.gecko.SiteIdentity.SecurityMode; @@ -16,32 +19,91 @@ import java.util.HashMap; import java.util.Map; /** - * Util class which encapsulate logic of how CustomTabsActivity treats SiteIdentity. - * TODO: Bug 1347037 - This class should be reusable for other components + * Util class to help on resolving SiteIdentity to get corresponding visual result. */ public class SecurityModeUtil { - // defined basic mapping between SecurityMode and SecurityModeUtil.Mode - private static final Map securityModeMap; + /** + * Abstract icon type for SiteIdentity resolving algorithm. Hence no need to worry about + * Drawable level value. + */ + public enum IconType { + UNKNOWN, + DEFAULT, + SEARCH, + LOCK_SECURE, + LOCK_WARNING, // not used for now. reserve for MixedDisplayContent icon, if any. + LOCK_INSECURE, + WARNING, + TRACKING_CONTENT_BLOCKED, + TRACKING_CONTENT_LOADED + } + + // Defined mapping between IconType and Drawable image-level + private static final Map imgLevelMap = new HashMap<>(); + + // http://dxr.mozilla.org/mozilla-central/source/mobile/android/base/java/org/mozilla/gecko/resources/drawable/site_security_icon.xml + static { + imgLevelMap.put(IconType.UNKNOWN, 0); + imgLevelMap.put(IconType.DEFAULT, 0); + imgLevelMap.put(IconType.LOCK_SECURE, 1); + imgLevelMap.put(IconType.WARNING, 2); + imgLevelMap.put(IconType.LOCK_INSECURE, 3); + imgLevelMap.put(IconType.TRACKING_CONTENT_BLOCKED, 4); + imgLevelMap.put(IconType.TRACKING_CONTENT_LOADED, 5); + imgLevelMap.put(IconType.SEARCH, 6); + } + + // defined basic mapping between SecurityMode and SecurityModeUtil.IconType + private static final Map securityModeMap; static { securityModeMap = new HashMap<>(); - securityModeMap.put(SecurityMode.UNKNOWN, Mode.UNKNOWN); - securityModeMap.put(SecurityMode.IDENTIFIED, Mode.LOCK_SECURE); - securityModeMap.put(SecurityMode.VERIFIED, Mode.LOCK_SECURE); - securityModeMap.put(SecurityMode.CHROMEUI, Mode.UNKNOWN); + securityModeMap.put(SecurityMode.UNKNOWN, IconType.UNKNOWN); + securityModeMap.put(SecurityMode.IDENTIFIED, IconType.LOCK_SECURE); + securityModeMap.put(SecurityMode.VERIFIED, IconType.LOCK_SECURE); + securityModeMap.put(SecurityMode.CHROMEUI, IconType.UNKNOWN); } /** - * To resolve which mode to be used for given SiteIdentity. Its logic is similar to - * ToolbarDisplayLayout.updateSiteIdentity + * Get image level from IconType, and to use in ImageView.setImageLevel(). + * + * @param type IconType which is resolved by method resolve() + * @return the image level which defined in Drawable + */ + public static int getImageLevel(@NonNull final IconType type) { + return imgLevelMap.containsKey(type) + ? imgLevelMap.get(type) + : imgLevelMap.get(IconType.UNKNOWN); + } + + /** + * To resolve which icon-type to be used for given SiteIdentity. * * @param identity An identity of a site to be resolved - * @return Corresponding mode for resolved SiteIdentity, UNKNOWN as default. + * @return Corresponding type for resolved SiteIdentity, UNKNOWN as default. */ - public static Mode resolve(final @Nullable SiteIdentity identity) { + public static IconType resolve(@Nullable final SiteIdentity identity) { + return resolve(identity, null); + } + + /** + * To resolve which icon-type to be used for given SiteIdentity. + * + * @param identity An identity of a site to be resolved + * @param url current page url + * @return Corresponding type for resolved SiteIdentity, UNKNOWN as default. + */ + public static IconType resolve(@Nullable final SiteIdentity identity, + @Nullable final String url) { + + if (!TextUtils.isEmpty(url) && AboutPages.isTitlelessAboutPage(url)) { + // We always want to just show a search icon on about:home + return IconType.SEARCH; + } + if (identity == null) { - return Mode.UNKNOWN; + return IconType.UNKNOWN; } final SecurityMode securityMode = identity.getSecurityMode(); @@ -50,35 +112,40 @@ public class SecurityModeUtil { final TrackingMode trackingMode = identity.getTrackingMode(); final boolean securityException = identity.isSecurityException(); - if (securityMode == SiteIdentity.SecurityMode.CHROMEUI) { - return Mode.UNKNOWN; + if (securityException) { + return IconType.WARNING; + } else if (trackingMode == TrackingMode.TRACKING_CONTENT_LOADED) { + return IconType.TRACKING_CONTENT_LOADED; + } else if (trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED) { + return IconType.TRACKING_CONTENT_BLOCKED; + } else if (activeMixedMode == MixedMode.LOADED) { + return IconType.LOCK_INSECURE; + } else if (displayMixedMode == MixedMode.LOADED) { + return IconType.WARNING; } - if (securityException) { - return Mode.MIXED_MODE; - } else if (trackingMode == TrackingMode.TRACKING_CONTENT_LOADED) { - return Mode.TRACKING_CONTENT_LOADED; - } else if (trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED) { - return Mode.TRACKING_CONTENT_BLOCKED; - } else if (activeMixedMode == MixedMode.LOADED) { - return Mode.MIXED_MODE; - } else if (displayMixedMode == MixedMode.LOADED) { - return Mode.WARNING; + // Chrome-UI checking is after tracking/mixed-content, even for about: pages, as they + // can still load external sites. + if (securityMode == SiteIdentity.SecurityMode.CHROMEUI) { + return IconType.DEFAULT; } return securityModeMap.containsKey(securityMode) ? securityModeMap.get(securityMode) - : Mode.UNKNOWN; + : IconType.UNKNOWN; } - // Security mode constants, which map to the icons / levels defined in: - // http://dxr.mozilla.org/mozilla-central/source/mobile/android/base/java/org/mozilla/gecko/resources/drawable/customtabs_site_security_level.xml - public enum Mode { - UNKNOWN, - LOCK_SECURE, - WARNING, - MIXED_MODE, - TRACKING_CONTENT_BLOCKED, - TRACKING_CONTENT_LOADED + /** + * For a given SiteIdentity, to check whether its tracking protection is enabled. + * + * @param identity to be checked + * @return true if tracking protection is enabled. + */ + public static boolean isTrackingProtectionEnabled(final @Nullable SiteIdentity identity) { + final TrackingMode trackingMode = (identity == null) + ? TrackingMode.UNKNOWN + : identity.getTrackingMode(); + + return (trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED); } } diff --git a/mobile/android/base/resources/drawable/customtabs_site_security_icon.xml b/mobile/android/base/resources/drawable/site_security_icon.xml similarity index 65% rename from mobile/android/base/resources/drawable/customtabs_site_security_icon.xml rename to mobile/android/base/resources/drawable/site_security_icon.xml index 3eb3e0fe122b..0a6934e40d85 100644 --- a/mobile/android/base/resources/drawable/customtabs_site_security_icon.xml +++ b/mobile/android/base/resources/drawable/site_security_icon.xml @@ -1,27 +1,32 @@ + + + android:maxLevel="0" /> + android:maxLevel="1" /> + android:maxLevel="2" /> + android:maxLevel="3" /> + android:maxLevel="4" /> + android:maxLevel="5" /> + diff --git a/mobile/android/base/resources/layout/customtabs_action_bar_custom_view.xml b/mobile/android/base/resources/layout/customtabs_action_bar_custom_view.xml index 94a79fda9627..8a24320b93ae 100644 --- a/mobile/android/base/resources/layout/customtabs_action_bar_custom_view.xml +++ b/mobile/android/base/resources/layout/customtabs_action_bar_custom_view.xml @@ -24,7 +24,7 @@ android:contentDescription="@string/site_security" android:padding="3dp" android:scaleType="fitCenter" - android:src="@drawable/customtabs_site_security_icon" + android:src="@drawable/site_security_icon" android:visibility="invisible"/> diff --git a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/toolbar/TestSecurityModeUtil.java b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/toolbar/TestSecurityModeUtil.java new file mode 100644 index 000000000000..fbc65d09944b --- /dev/null +++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/toolbar/TestSecurityModeUtil.java @@ -0,0 +1,231 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +package org.mozilla.gecko.toolbar; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mozilla.gecko.SiteIdentity; +import org.mozilla.gecko.SiteIdentity.MixedMode; +import org.mozilla.gecko.SiteIdentity.SecurityMode; +import org.mozilla.gecko.SiteIdentity.TrackingMode; +import org.mozilla.gecko.background.testhelpers.TestRunner; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mozilla.gecko.toolbar.SecurityModeUtil.IconType; +import static org.mozilla.gecko.toolbar.SecurityModeUtil.resolve; + +@RunWith(TestRunner.class) +public class TestSecurityModeUtil { + + private SiteIdentity identity; + + @Before + public void setUp() { + identity = spy(new SiteIdentity()); + } + + /** + * To test resolve function if there is not any SiteIdentity + */ + @Test + public void testNoSiteIdentity() { + assertEquals(IconType.UNKNOWN, resolve(null)); + assertEquals(IconType.SEARCH, resolve(null, "about:home")); + assertEquals(IconType.UNKNOWN, resolve(null, "about:firefox")); + assertEquals(IconType.UNKNOWN, resolve(null, "https://mozilla.com")); + } + + /** + * To test resolve function for SecurityException. + * SecurityException detection is prior than SecurityMode, TrackingMode and MixedMode. + * If SecurityException exists, it should keep returning WARNING until url is "about:home". + */ + @Test + public void testSecurityException() { + // SecurityException exists for each of below cases + doReturn(true).when(identity).isSecurityException(); + doReturn(SecurityMode.UNKNOWN).when(identity).getSecurityMode(); + + // about:home always show Search Icon + assertEquals(IconType.SEARCH, resolve(identity, "about:home")); + + // other cases, return WARNING + assertEquals(IconType.WARNING, resolve(identity)); + assertEquals(IconType.WARNING, resolve(identity, "https://mozilla.com")); + + // even about:* pages, they can still load external sites. + assertEquals(IconType.WARNING, resolve(identity, "about:firefox")); + // even specify ChromeUI, still respect SecurityException + doReturn(SecurityMode.CHROMEUI).when(identity).getSecurityMode(); + assertEquals(IconType.WARNING, resolve(identity, "about:firefox")); + + // TrackingMode does not matter + doReturn(TrackingMode.TRACKING_CONTENT_BLOCKED).when(identity).getTrackingMode(); + assertEquals(IconType.WARNING, resolve(identity)); + doReturn(TrackingMode.TRACKING_CONTENT_LOADED).when(identity).getTrackingMode(); + assertEquals(IconType.WARNING, resolve(identity)); + + // MixedModeDisplay does not matter + doReturn(MixedMode.LOADED).when(identity).getMixedModeDisplay(); + assertEquals(IconType.WARNING, resolve(identity)); + + doReturn(MixedMode.BLOCKED).when(identity).getMixedModeDisplay(); + assertEquals(IconType.WARNING, resolve(identity)); + + // MixedModeActive does not matter + doReturn(MixedMode.LOADED).when(identity).getMixedModeActive(); + assertEquals(IconType.WARNING, resolve(identity)); + + doReturn(MixedMode.BLOCKED).when(identity).getMixedModeActive(); + assertEquals(IconType.WARNING, resolve(identity)); + + } + + /** + * To test resolve function for TrackingContentLoaded, without SecurityException. + * TrackingMode detection is prior than SecurityMode and MixedMode. + * If TrackingMode is TRACKING_CONTENT_LOADED, it should keep returning TRACKING_CONTENT_LOADED + * icon until url is "about:home". + */ + @Test + public void testTrackingContentLoaded() { + doReturn(false).when(identity).isSecurityException(); + + // enable TRACKING_CONTENT_LOADED + doReturn(TrackingMode.TRACKING_CONTENT_LOADED).when(identity).getTrackingMode(); + + // about:home always show Search Icon + doReturn(SecurityMode.UNKNOWN).when(identity).getSecurityMode(); + assertEquals(IconType.SEARCH, resolve(identity, "about:home")); + + // otherwise, return icon for TRACKING_CONTENT_LOADED + assertEquals(IconType.TRACKING_CONTENT_LOADED, resolve(identity)); + assertEquals(IconType.TRACKING_CONTENT_LOADED, resolve(identity, "https://mozilla.com")); + + // even for SecurityMode.CHROMEUI, TrackingMode still prior than SecurityMode. + doReturn(SecurityMode.CHROMEUI).when(identity).getSecurityMode(); + assertEquals(IconType.TRACKING_CONTENT_LOADED, resolve(identity)); + + // even for SecurityMode.VERIFIED, TrackingMode still prior than SecurityMode. + doReturn(SecurityMode.VERIFIED).when(identity).getSecurityMode(); + assertEquals(IconType.TRACKING_CONTENT_LOADED, resolve(identity)); + + // even for SecurityMode.IDENTIFIED, TrackingMode still prior than SecurityMode. + doReturn(SecurityMode.IDENTIFIED).when(identity).getSecurityMode(); + assertEquals(IconType.TRACKING_CONTENT_LOADED, resolve(identity)); + } + + /** + * To test resolve function for TrackingContentBlocked, without SecurityException. + * TrackingMode detection is prior than SecurityMode and MixedMode. + * If TrackingMode is TRACKING_CONTENT_BLOCKED, it should keep returning TRACKING_CONTENT_BLOCKED + * icon until url is "about:home". + */ + @Test + public void testTrackingContentBlocked() { + doReturn(false).when(identity).isSecurityException(); + + // enable TRACKING_CONTENT_BLOCKED + doReturn(TrackingMode.TRACKING_CONTENT_BLOCKED).when(identity).getTrackingMode(); + + // about:home always show Search Icon + doReturn(SecurityMode.UNKNOWN).when(identity).getSecurityMode(); + assertEquals(IconType.SEARCH, resolve(identity, "about:home")); + + // otherwise, return icon for TRACKING_CONTENT_BLOCKED + assertEquals(IconType.TRACKING_CONTENT_BLOCKED, resolve(identity)); + + // even for SecurityMode.CHROMEUI, TrackingMode still prior than SecurityMode. + doReturn(SecurityMode.CHROMEUI).when(identity).getSecurityMode(); + assertEquals(IconType.TRACKING_CONTENT_BLOCKED, resolve(identity)); + + // even for SecurityMode.VERIFIED, TrackingMode still prior than SecurityMode. + doReturn(SecurityMode.VERIFIED).when(identity).getSecurityMode(); + assertEquals(IconType.TRACKING_CONTENT_BLOCKED, resolve(identity)); + + // even for SecurityMode.IDENTIFIED, TrackingMode still prior than SecurityMode. + doReturn(SecurityMode.IDENTIFIED).when(identity).getSecurityMode(); + assertEquals(IconType.TRACKING_CONTENT_BLOCKED, resolve(identity)); + } + + /** + * To test resolve function for MixedMode, without SecurityException nor TrackingMode. + * MixedMode detection is prior than SecurityMode. And in MixedMode, MixedActiveContent + * is prior than MixedDisplayContent. + */ + @Test + public void testMixedMode() { + doReturn(false).when(identity).isSecurityException(); + doReturn(TrackingMode.UNKNOWN).when(identity).getTrackingMode(); + + // MixedActiveContent loaded, MixedDisplayContent loaded + doReturn(MixedMode.LOADED).when(identity).getMixedModeActive(); + doReturn(MixedMode.LOADED).when(identity).getMixedModeDisplay(); + + // about:home always show Search Icon + doReturn(SecurityMode.UNKNOWN).when(identity).getSecurityMode(); + assertEquals(IconType.SEARCH, resolve(identity, "about:home")); + // otherwise, return icon for MixedModeActive-loaded + assertEquals(IconType.LOCK_INSECURE, resolve(identity)); + + // MixedActiveContent loaded, MixedDisplayContent blocked + // It should be same as ActiveContent-loaded-DisplayContent-loaded + doReturn(MixedMode.LOADED).when(identity).getMixedModeActive(); + doReturn(MixedMode.BLOCKED).when(identity).getMixedModeDisplay(); + assertEquals(IconType.LOCK_INSECURE, resolve(identity)); + + // MixedActiveContent blocked, MixedDisplayContent loaded + doReturn(MixedMode.BLOCKED).when(identity).getMixedModeActive(); + doReturn(MixedMode.LOADED).when(identity).getMixedModeDisplay(); + assertEquals(IconType.WARNING, resolve(identity)); + } + + /** + * To test resolve function for SecurityMode, without SecurityException nor TrackingMode nor + * MixedMode. + */ + @Test + public void testSecurityMode() { + doReturn(false).when(identity).isSecurityException(); + doReturn(TrackingMode.UNKNOWN).when(identity).getTrackingMode(); + doReturn(MixedMode.BLOCKED).when(identity).getMixedModeActive(); + doReturn(MixedMode.BLOCKED).when(identity).getMixedModeDisplay(); + + // about:home always show Search Icon + doReturn(SecurityMode.UNKNOWN).when(identity).getSecurityMode(); + assertEquals(IconType.SEARCH, resolve(identity, "about:home")); + + // for SecurityMode.CHROMEUI, We should show a global icon + doReturn(SecurityMode.CHROMEUI).when(identity).getSecurityMode(); + assertEquals(IconType.DEFAULT, resolve(identity)); + + // for SecurityMode.VERIFIED, We should show a VERIFIED icon + doReturn(SecurityMode.VERIFIED).when(identity).getSecurityMode(); + assertEquals(IconType.LOCK_SECURE, resolve(identity)); + + // for SecurityMode.IDENTIFIED, We should show a IDENTIFIED icon + doReturn(SecurityMode.IDENTIFIED).when(identity).getSecurityMode(); + assertEquals(IconType.LOCK_SECURE, resolve(identity)); + } + + @Test + public void testIsTrackingProtectionEnabled() { + // no identity, the tracking protection should be regard as disabled + Assert.assertFalse(SecurityModeUtil.isTrackingProtectionEnabled(null)); + + doReturn(TrackingMode.UNKNOWN).when(identity).getTrackingMode(); + Assert.assertFalse(SecurityModeUtil.isTrackingProtectionEnabled(identity)); + + doReturn(TrackingMode.TRACKING_CONTENT_LOADED).when(identity).getTrackingMode(); + Assert.assertFalse(SecurityModeUtil.isTrackingProtectionEnabled(identity)); + + doReturn(TrackingMode.TRACKING_CONTENT_BLOCKED).when(identity).getTrackingMode(); + Assert.assertTrue(SecurityModeUtil.isTrackingProtectionEnabled(identity)); + } + +}