Bug 1377583 - Expose security information in custom tabs. r=snorp

This adds a class, CustomTabsSecurityPopup, which allows us to display information from the security certificate on secure sites.
This commit is contained in:
Dylan Roeh 2017-08-10 09:10:57 -05:00
parent 4e2ab7b208
commit 0740754325
5 changed files with 354 additions and 35 deletions

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingLeft="@dimen/doorhanger_section_padding_small"
android:paddingRight="@dimen/doorhanger_section_padding_small"
android:paddingBottom="@dimen/doorhanger_section_padding_large"
android:paddingTop="@dimen/doorhanger_section_padding_medium">
<ImageView android:id="@+id/site_identity_icon"
android:layout_width="@dimen/doorhanger_icon_size"
android:layout_height="@dimen/doorhanger_icon_size"
android:gravity="center_horizontal"
android:padding="@dimen/doorhanger_section_padding_small"
android:layout_marginRight="@dimen/doorhanger_section_padding_small"/>
<LinearLayout android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_weight="1.0">
<TextView android:id="@+id/site_identity_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.DoorHanger.Medium.Light"/>
<TextView android:id="@+id/site_identity_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/doorhanger_subsection_padding"
android:textAppearance="@style/TextAppearance.DoorHanger.Medium.Bold"/>
<TextView android:id="@+id/mixed_content_activity"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/doorhanger_section_padding_medium"
android:textAppearance="@style/TextAppearance.DoorHanger.Medium"
android:visibility="gone"/>
<LinearLayout android:id="@+id/site_identity_known_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:orientation="vertical">
<TextView android:id="@+id/owner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/doorhanger_section_padding_small"
android:textAppearance="@style/TextAppearance.DoorHanger.Medium"
android:textStyle="bold"/>
<TextView android:id="@+id/verifier"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/doorhanger_section_padding_medium"
android:textAppearance="@style/TextAppearance.DoorHanger.Medium.Light"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@ -29,11 +29,12 @@ import android.widget.ImageView;
import android.widget.TextView;
import org.mozilla.gecko.GeckoView;
import org.mozilla.gecko.GeckoView.ProgressListener.SecurityInformation;
import org.mozilla.gecko.R;
import org.mozilla.gecko.SiteIdentity;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.toolbar.SecurityModeUtil;
import org.mozilla.gecko.toolbar.SiteIdentityPopup;
import org.mozilla.gecko.toolbar.CustomTabsSecurityPopup;
import org.mozilla.gecko.util.ColorUtil;
/**
@ -47,7 +48,7 @@ public class ActionBarPresenter {
private static final long CUSTOM_VIEW_UPDATE_DELAY = 1000;
private final ActionBar mActionBar;
private final SiteIdentityPopup mIdentityPopup;
private final CustomTabsSecurityPopup mIdentityPopup;
private final ImageButton mIconView;
private final TextView mTitleView;
private final TextView mUrlView;
@ -73,7 +74,7 @@ public class ActionBarPresenter {
mTitleView.setTextColor(mTextPrimaryColor);
mUrlView.setTextColor(mTextPrimaryColor);
mIdentityPopup = new SiteIdentityPopup(mActionBar.getThemedContext());
mIdentityPopup = new CustomTabsSecurityPopup(mActionBar.getThemedContext());
mIdentityPopup.setAnchor(customView);
mIconView.setOnClickListener(new View.OnClickListener() {
@Override
@ -85,29 +86,13 @@ public class ActionBarPresenter {
initIndicator();
}
/**
* Called when ActionBar is to start interacting with user. Usually this method is called from
* Activity.onResume.
*/
public void onResume() {
mIdentityPopup.registerListeners();
}
/**
* Called when ActionBar is going to background, but has not yet been killed. Usually this method
* is called from Activity.onPause.
*/
public void onPause() {
mIdentityPopup.unregisterListeners();
}
/**
* To display Url in CustomView only and immediately.
*
* @param url Url String to display
*/
public void displayUrlOnly(@NonNull final String url) {
updateCustomView(null, url, /* isSecure */ false);
updateCustomView(null, url, /* security */ null);
}
/**
@ -115,16 +100,16 @@ public class ActionBarPresenter {
*
* @param title Title for current website. Could be null if don't want to show title.
* @param url URL for current website. At least Custom will show this url.
* @param isSecure A boolean representing whether or not the site is secure.
* @param security A SecurityInformation object giving the current security information
*/
public void update(final String title, final String url, final boolean isSecure) {
public void update(final String title, final String url, final SecurityInformation security) {
// Do not update CustomView immediately. If this method be invoked rapidly several times,
// only apply last one.
mHandler.removeCallbacks(mUpdateAction);
mUpdateAction = new Runnable() {
@Override
public void run() {
updateCustomView(title, url, isSecure);
updateCustomView(title, url, security);
}
};
mHandler.postDelayed(mUpdateAction, CUSTOM_VIEW_UPDATE_DELAY);
@ -215,18 +200,30 @@ public class ActionBarPresenter {
*
* @param title Title for current website. Could be null if don't want to show title.
* @param url URL for current website. At least Custom will show this url.
* @param isSecure A boolean representing whether or not the site is secure.
* @param security A SecurityInformation object giving the current security information
*/
@UiThread
private void updateCustomView(final String title, final String url, final boolean isSecure) {
if (isSecure) {
mIconView.setVisibility(View.VISIBLE);
mIconView.setImageLevel(SecurityModeUtil.getImageLevel(SecurityModeUtil.IconType.LOCK_SECURE));
// Lock-Secure is special case. Keep its original green color.
DrawableCompat.setTintList(mIconView.getDrawable(), null);
} else {
private void updateCustomView(final String title, final String url, final SecurityInformation security) {
if (security == null) {
mIconView.setVisibility(View.INVISIBLE);
DrawableCompat.setTint(mIconView.getDrawable(), mTextPrimaryColor);
} else {
SecurityModeUtil.IconType icon;
if ("unknown".equals(security.securityMode)) {
icon = SecurityModeUtil.IconType.UNKNOWN;
} else {
icon = SecurityModeUtil.IconType.LOCK_SECURE;
}
mIconView.setVisibility(View.VISIBLE);
mIconView.setImageLevel(SecurityModeUtil.getImageLevel(icon));
mIdentityPopup.setSecurityInformation(security);
if (icon == SecurityModeUtil.IconType.LOCK_SECURE) {
// Lock-Secure is a special case. Keep its original green color.
DrawableCompat.setTintList(mIconView.getDrawable(), null);
} else {
// Icon uses same color as TextView.
DrawableCompat.setTint(mIconView.getDrawable(), mTextPrimaryColor);
}
}
// If no title to use, use Url as title

View File

@ -77,7 +77,7 @@ public class CustomTabsActivity extends AppCompatActivity
private boolean mCanStop = false;
private String mCurrentUrl;
private String mCurrentTitle;
private boolean mIsSecure = false;
private SecurityInformation mSecurityInformation = null;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -424,7 +424,7 @@ public class CustomTabsActivity extends AppCompatActivity
* Update the state of the action bar
*/
private void updateActionBar() {
actionBarPresenter.update(mCurrentTitle, mCurrentUrl, mIsSecure);
actionBarPresenter.update(mCurrentTitle, mCurrentUrl, mSecurityInformation);
}
/**
@ -549,7 +549,7 @@ public class CustomTabsActivity extends AppCompatActivity
@Override
public void onSecurityChange(GeckoView view, SecurityInformation securityInfo) {
mIsSecure = securityInfo.isSecure;
mSecurityInformation = securityInfo;
updateActionBar();
}

View File

@ -0,0 +1,250 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.toolbar;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.TextViewCompat;
import android.widget.ImageView;
import android.widget.Toast;
import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoView;
import org.mozilla.gecko.GeckoView.ProgressListener.SecurityInformation;
import org.mozilla.gecko.R;
import org.mozilla.gecko.SiteIdentity;
import org.mozilla.gecko.SiteIdentity.SecurityMode;
import org.mozilla.gecko.SiteIdentity.MixedMode;
import org.mozilla.gecko.SiteIdentity.TrackingMode;
import org.mozilla.gecko.SnackbarBuilder;
import org.mozilla.gecko.Tab;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.widget.AnchoredPopup;
import org.mozilla.gecko.widget.DoorHanger;
import org.mozilla.gecko.widget.DoorHanger.OnButtonClickListener;
import android.app.Activity;
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.mozilla.gecko.widget.DoorhangerConfig;
import org.mozilla.gecko.widget.SiteLogins;
/**
* CustomTabsSecurityPopup is a singleton class that displays site identity data in
* an arrow panel popup hanging from the lock icon in the browser toolbar.
*
* A site identity icon may be displayed in the url, and is set in <code>ToolbarDisplayLayout</code>.
*/
public class CustomTabsSecurityPopup extends AnchoredPopup {
private static final String LOGTAG = "CustomTabsSecurityPopup";
private final Resources mResources;
private SecurityInformation mSecurityInformation;
private LinearLayout mIdentity;
private LinearLayout mIdentityKnownContainer;
private ImageView mIcon;
private TextView mTitle;
private TextView mSecurityState;
private TextView mMixedContentActivity;
private TextView mOwner;
private TextView mVerifier;
public CustomTabsSecurityPopup(Context context) {
super(context);
mResources = mContext.getResources();
}
@Override
protected void init() {
super.init();
// Make the popup focusable so it doesn't inadvertently trigger click events elsewhere
// which may reshow the popup (see bug 785156)
setFocusable(true);
LayoutInflater inflater = LayoutInflater.from(mContext);
mIdentity = (LinearLayout) inflater.inflate(R.layout.customtabs_site_identity, null);
mContent.addView(mIdentity);
mIdentityKnownContainer =
(LinearLayout) mIdentity.findViewById(R.id.site_identity_known_container);
mIcon = (ImageView) mIdentity.findViewById(R.id.site_identity_icon);
mTitle = (TextView) mIdentity.findViewById(R.id.site_identity_title);
mSecurityState = (TextView) mIdentity.findViewById(R.id.site_identity_state);
mMixedContentActivity = (TextView) mIdentity.findViewById(R.id.mixed_content_activity);
mOwner = (TextView) mIdentityKnownContainer.findViewById(R.id.owner);
mVerifier = (TextView) mIdentityKnownContainer.findViewById(R.id.verifier);
}
private void updateSecurityInformation(final SecurityInformation security) {
if (!mInflated) {
init();
}
final boolean isIdentityKnown = ("identified".equals(security.securityMode) ||
"verified".equals(security.securityMode));
updateConnectionState(security);
toggleIdentityKnownContainerVisibility(isIdentityKnown);
if (isIdentityKnown) {
updateIdentityInformation(security);
}
}
private void toggleIdentityKnownContainerVisibility(final boolean isIdentityKnown) {
final int identityInfoVisibility = isIdentityKnown ? View.VISIBLE : View.GONE;
mIdentityKnownContainer.setVisibility(identityInfoVisibility);
}
/**
* Update the SecurityInformation content to reflect connection state.
*
* The connection state should reflect the combination of:
* a) Connection encryption
* b) Mixed Content state (Active/Display Mixed content, loaded, blocked, none, etc)
* and update the icons and strings to inform the user of that state.
*
* @param security SecurityInformation about the connection.
*/
private void updateConnectionState(final SecurityInformation security) {
if (!security.isSecure) {
if ("loaded".equals(security.mixedModeActive)) {
// Active Mixed Content loaded because user has disabled blocking.
mIcon.setImageResource(R.drawable.ic_lock_disabled);
clearSecurityStateIcon();
mMixedContentActivity.setVisibility(View.VISIBLE);
mMixedContentActivity.setText(R.string.mixed_content_protection_disabled);
} else if ("loaded".equals(security.mixedModePassive)) {
// Passive Mixed Content loaded.
mIcon.setImageResource(R.drawable.ic_lock_inactive);
setSecurityStateIcon(R.drawable.ic_warning_major, 1);
mMixedContentActivity.setVisibility(View.VISIBLE);
if ("blocked".equals(security.mixedModeActive)) {
mMixedContentActivity.setText(R.string.mixed_content_blocked_some);
} else {
mMixedContentActivity.setText(R.string.mixed_content_display_loaded);
}
} else {
// Unencrypted connection with no mixed content.
mIcon.setImageResource(R.drawable.globe_light);
clearSecurityStateIcon();
mMixedContentActivity.setVisibility(View.GONE);
}
mSecurityState.setText(R.string.identity_connection_insecure);
mSecurityState.setTextColor(ContextCompat.getColor(mContext, R.color.placeholder_active_grey));
} else if (security.isException) {
mIcon.setImageResource(R.drawable.ic_lock_inactive);
setSecurityStateIcon(R.drawable.ic_warning_major, 1);
mSecurityState.setText(R.string.identity_connection_insecure);
mSecurityState.setTextColor(ContextCompat.getColor(mContext, R.color.placeholder_active_grey));
} else {
// Connection is secure.
mIcon.setImageResource(R.drawable.ic_lock);
setSecurityStateIcon(R.drawable.img_check, 2);
mSecurityState.setTextColor(ContextCompat.getColor(mContext, R.color.affirmative_green));
mSecurityState.setText(R.string.identity_connection_secure);
// Mixed content has been blocked, if present.
if ("blocked".equals(security.mixedModeActive) ||
"blocked".equals(security.mixedModePassive)) {
mMixedContentActivity.setVisibility(View.VISIBLE);
mMixedContentActivity.setText(R.string.mixed_content_blocked_all);
} else {
mMixedContentActivity.setVisibility(View.GONE);
}
}
}
private void clearSecurityStateIcon() {
mSecurityState.setCompoundDrawablePadding(0);
TextViewCompat.setCompoundDrawablesRelative(mSecurityState, null, null, null, null);
}
private void setSecurityStateIcon(int resource, int factor) {
final Drawable stateIcon = ContextCompat.getDrawable(mContext, resource);
stateIcon.setBounds(0, 0, stateIcon.getIntrinsicWidth() / factor, stateIcon.getIntrinsicHeight() / factor);
TextViewCompat.setCompoundDrawablesRelative(mSecurityState, stateIcon, null, null, null);
mSecurityState.setCompoundDrawablePadding((int) mResources.getDimension(R.dimen.doorhanger_drawable_padding));
}
private void updateIdentityInformation(final SecurityInformation security) {
String owner = security.organization;
if (owner == null) {
mOwner.setVisibility(View.GONE);
} else {
mOwner.setText(owner);
mOwner.setVisibility(View.VISIBLE);
}
// TODO: This will differ from Fennec currently, as SiteIdentityPopup uses a localized string
mVerifier.setText(security.issuerCommonName);
}
/*
* @param identityData An object that holds the current tab's identity data.
*/
public void setSecurityInformation(SecurityInformation security) {
mSecurityInformation = security;
}
@Override
public void show() {
if (mSecurityInformation == null) {
Log.e(LOGTAG, "Can't show site identity popup for undefined state");
return;
}
updateSecurityInformation(mSecurityInformation);
mTitle.setText(mSecurityInformation.host);
// TODO: Might be nice to revive this at some point, but very low priority.
/*final Bitmap favicon = selectedTab.getFavicon();
if (favicon != null) {
final Drawable faviconDrawable = new BitmapDrawable(mResources, favicon);
final int dimen = (int) mResources.getDimension(R.dimen.browser_toolbar_favicon_size);
faviconDrawable.setBounds(0, 0, dimen, dimen);
TextViewCompat.setCompoundDrawablesRelative(mTitle, faviconDrawable, null, null, null);
mTitle.setCompoundDrawablePadding((int) mContext.getResources().getDimension(R.dimen.doorhanger_drawable_padding));
}*/
super.show();
}
@Override
public void dismiss() {
super.dismiss();
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(mTitle, null, null, null, null);
}
}

View File

@ -554,6 +554,7 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'CustomEditText.java',
'customtabs/ActionBarPresenter.java',
'customtabs/CustomTabsActivity.java',
'customtabs/CustomTabsSecurityPopup.java',
'customtabs/GeckoCustomTabsService.java',
'customtabs/IntentUtil.java',
'DataReportingNotification.java',