Bug 1088220 - Add login doorhanger. r=margaret

--HG--
extra : rebase_source : 567e4817240b9d5a99b73e9f7cdd514ab1203bb3
This commit is contained in:
Chenxia Liu 2015-03-26 16:34:29 -07:00
parent 28b8a7c66c
commit 3b2de6bae9
11 changed files with 243 additions and 54 deletions

View File

@ -115,8 +115,8 @@ public class DoorHangerPopup extends AnchoredPopup
config.setButtons(json.getJSONArray("buttons"));
config.setOptions(json.getJSONObject("options"));
final String typeString = json.optString("category");
if (DoorHanger.Type.PASSWORD.toString().equals(typeString)) {
config.setType(DoorHanger.Type.PASSWORD);
if (DoorHanger.Type.LOGIN.toString().equals(typeString)) {
config.setType(DoorHanger.Type.LOGIN);
}
return config;

View File

@ -505,6 +505,7 @@ gbjar.sources += [
'widget/GeckoSwipeRefreshLayout.java',
'widget/GeckoViewFlipper.java',
'widget/IconTabWidget.java',
'widget/LoginDoorHanger.java',
'widget/ResizablePathDrawable.java',
'widget/SquaredImageView.java',
'widget/SwipeDismissListViewTouchListener.java',

View File

@ -20,7 +20,7 @@
android:focusable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.Widget.DoorHanger.Medium"/>
android:textAppearance="@style/TextAppearance.DoorHanger.Medium"/>
</LinearLayout>

View File

@ -0,0 +1,67 @@
<?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/. -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingTop="@dimen/doorhanger_section_padding_small"
android:paddingLeft="@dimen/doorhanger_section_padding_small">
<ImageView android:id="@+id/doorhanger_icon"
android:layout_width="@dimen/doorhanger_icon_size"
android:layout_height="@dimen/doorhanger_icon_size"
android:layout_gravity="center_horizontal"
android:paddingRight="@dimen/doorhanger_section_padding_small"
android:src="@drawable/icon_key"/>
<LinearLayout android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView android:id="@+id/doorhanger_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/doorhanger_section_padding_small"
android:textAppearance="@style/TextAppearance.DoorHanger.Medium.Light"/>
<TextView android:id="@+id/doorhanger_message"
android:focusable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/doorhanger_section_padding_large"
android:textAppearance="@style/TextAppearance.DoorHanger.Medium"/>
<TextView android:id="@+id/doorhanger_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.DoorHanger.Medium"
android:paddingBottom="@dimen/doorhanger_section_padding_large"
android:visibility="gone"/>
</LinearLayout>
</LinearLayout>
<View android:id="@+id/divider_buttons"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider_light"
android:visibility="gone"/>
<LinearLayout android:id="@+id/doorhanger_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone"/>
<View android:id="@+id/divider_doorhanger"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/divider_light"
android:visibility="gone"/>
</merge>

View File

@ -100,6 +100,10 @@
<dimen name="doorhanger_padding">15dp</dimen>
<dimen name="doorhanger_offsetX">10dp</dimen>
<dimen name="doorhanger_offsetY">7dp</dimen>
<dimen name="doorhanger_drawable_padding">5dp</dimen>
<dimen name="doorhanger_section_padding_small">20dp</dimen>
<dimen name="doorhanger_section_padding_large">30dp</dimen>
<dimen name="doorhanger_icon_size">60dp</dimen>
<dimen name="flow_layout_spacing">6dp</dimen>
<dimen name="menu_item_icon">21dp</dimen>

View File

@ -410,14 +410,21 @@
<item name="android:textColor">?android:attr/textColorHint</item>
</style>
<style name="TextAppearance.Widget.DoorHanger.Medium" parent="TextAppearance.Medium">
<style name="TextAppearance.DoorHanger">
<item name="android:textColor">@color/placeholder_active_grey</item>
<item name="android:textColorLink">@color/doorhanger_link</item>
</style>
<style name="TextAppearance.Widget.DoorHanger.Small" parent="TextAppearance.Small">
<item name="android:textColor">@color/placeholder_active_grey</item>
<item name="android:textColorLink">@color/doorhanger_link</item>
<style name="TextAppearance.DoorHanger.Medium">
<item name="android:textSize">16dp</item>
</style>
<style name="TextAppearance.DoorHanger.Medium.Light">
<item name="android:fontFamily">sans-serif-light</item>
</style>
<style name="TextAppearance.DoorHanger.Small">
<item name="android:textSize">14sp</item>
</style>
<style name="TextAppearance.UrlBar.Title" parent="TextAppearance.Small">

View File

@ -32,7 +32,8 @@ public abstract class DoorHanger extends LinearLayout {
final Type type = config.getType();
if (type != null) {
switch (type) {
case PASSWORD:
case LOGIN:
return new LoginDoorHanger(context, config);
case SITE:
return new DefaultDoorHanger(context, config, type);
}
@ -41,7 +42,7 @@ public abstract class DoorHanger extends LinearLayout {
return new DefaultDoorHanger(context, config);
}
public static enum Type { DEFAULT, PASSWORD, SITE }
public static enum Type { DEFAULT, LOGIN, SITE }
public interface OnButtonClickListener {
public void onButtonClick(DoorHanger dh, String tag);
@ -84,9 +85,8 @@ public abstract class DoorHanger extends LinearLayout {
int resource;
switch (type) {
case PASSWORD:
// TODO: switch to R.layout.password
resource = R.layout.doorhanger;
case LOGIN:
resource = R.layout.login_doorhanger;
break;
default:
resource = R.layout.doorhanger;
@ -97,7 +97,7 @@ public abstract class DoorHanger extends LinearLayout {
mIcon = (ImageView) findViewById(R.id.doorhanger_icon);
mMessage = (TextView) findViewById(R.id.doorhanger_message);
if (type == Type.SITE) {
mMessage.setTextAppearance(getContext(), R.style.TextAppearance_Widget_DoorHanger_Small);
mMessage.setTextAppearance(getContext(), R.style.TextAppearance_DoorHanger_Small);
}
mButtonsContainer = (LinearLayout) findViewById(R.id.doorhanger_buttons);

View File

@ -0,0 +1,79 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* 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.widget;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import ch.boye.httpclientandroidlib.util.TextUtils;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.R;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
public class LoginDoorHanger extends DoorHanger {
private static final String LOGTAG = "LoginDoorHanger";
final TextView mTitle;
final TextView mLogin;
public LoginDoorHanger(Context context, DoorhangerConfig config) {
super(context, config, Type.LOGIN);
mTitle = (TextView) findViewById(R.id.doorhanger_title);
mLogin = (TextView) findViewById(R.id.doorhanger_login);
loadConfig(config);
}
@Override
protected void loadConfig(DoorhangerConfig config) {
setOptions(config.getOptions());
setMessage(config.getMessage());
}
@Override
protected void setOptions(final JSONObject options) {
super.setOptions(options);
final JSONObject titleObj = options.optJSONObject("title");
if (titleObj != null) {
try {
final String text = titleObj.getString("text");
mTitle.setText(text);
} catch (JSONException e) {
Log.e(LOGTAG, "Error loading title from options JSON");
}
final String resource = titleObj.optString("resource");
if (resource != null) {
Favicons.getSizedFaviconForPageFromLocal(mContext, resource, 32, new OnFaviconLoadedListener() {
@Override
public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
if (favicon != null) {
mTitle.setCompoundDrawablesWithIntrinsicBounds(new BitmapDrawable(mContext.getResources(), favicon), null, null, null);
mTitle.setCompoundDrawablePadding((int) mContext.getResources().getDimension(R.dimen.doorhanger_drawable_padding));
}
}
});
}
}
final String subtext = options.optString("subtext");
if (!TextUtils.isEmpty(subtext)) {
mLogin.setText(subtext);
mLogin.setVisibility(View.VISIBLE);
} else {
mLogin.setVisibility(View.GONE);
}
}
}

View File

@ -2213,11 +2213,24 @@ var NativeWindow = {
* persist across location changes.
* timeout: A time in milliseconds. The notification will not
* automatically dismiss before this time.
*
* checkbox: A string to appear next to a checkbox under the notification
* message. The button callback functions will be called with
* the checked state as an argument.
*
* title: An object that specifies text to display as the title, and
* optionally a resource, such as a favicon cache url that can be
* used to fetch a favicon from the FaviconCache. (This can be
* generalized to other resources if the situation arises.)
* { text: <title>,
* resource: <resource_url> }
*
* subtext: A string to appear below the doorhanger message.
*
* @param aCategory
* Doorhanger type to display (e.g., LOGIN)
*/
show: function(aMessage, aValue, aButtons, aTabID, aOptions) {
show: function(aMessage, aValue, aButtons, aTabID, aOptions, aCategory) {
if (aButtons == null) {
aButtons = [];
}
@ -2236,7 +2249,8 @@ var NativeWindow = {
buttons: aButtons,
// use the current tab if none is provided
tabID: aTabID || BrowserApp.selectedTab.id,
options: aOptions || {}
options: aOptions || {},
category: aCategory
};
Messaging.sendRequest(json);
},

View File

@ -63,8 +63,12 @@ LoginManagerPrompter.prototype = {
if (!this.__strBundle) {
var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
getService(Ci.nsIStringBundleService);
this.__strBundle = bunService.createBundle(
"chrome://passwordmgr/locale/passwordmgr.properties");
this.__strBundle = {
pwmgr : bunService.createBundle(
"chrome://passwordmgr/locale/passwordmgr.properties"),
brand : bunService.createBundle("chrome://branding/locale/brand.properties")
};
if (!this.__strBundle)
throw "String bundle for Login Manager not present!";
}
@ -136,9 +140,19 @@ LoginManagerPrompter.prototype = {
* _showLoginNotification
*
* Displays a notification doorhanger.
* @param aName
* Name of notification
* @param aTitle
* Object with title and optional resource to display with the title, such as a favicon key
* @param aBody
* String message to be displayed in the doorhanger
* @param aButtons
* Buttons to display with the doorhanger
* @param aSubtext
* String to be displayed below the aBody message
*
*/
_showLoginNotification : function (aName, aText, aButtons) {
_showLoginNotification : function (aName, aTitle, aBody, aButtons, aSubtext) {
this.log("Adding new " + aName + " notification bar");
let notifyWin = this._window.top;
let chromeWin = this._getChromeWindow(notifyWin).wrappedJSObject;
@ -155,12 +169,14 @@ LoginManagerPrompter.prototype = {
let options = {
persistWhileVisible: true,
timeout: Date.now() + 10000
timeout: Date.now() + 10000,
title: aTitle,
subtext: aSubtext
}
var nativeWindow = this._getNativeWindow();
if (nativeWindow)
nativeWindow.doorhanger.show(aText, aName, aButtons, tabID, options);
nativeWindow.doorhanger.show(aBody, aName, aButtons, tabID, options, "LOGIN");
},
@ -173,15 +189,16 @@ LoginManagerPrompter.prototype = {
*
*/
_showSaveLoginNotification : function (aLogin) {
var displayHost = this._getShortDisplayHost(aLogin.hostname);
var notificationText;
if (aLogin.username) {
var displayUser = this._sanitizeUsername(aLogin.username);
notificationText = this._getLocalizedString("savePassword", [displayUser, displayHost]);
} else {
notificationText = this._getLocalizedString("savePasswordNoUser", [displayHost]);
}
let brandShortName = this._strBundle.brand.GetStringFromName("brandShortName");
let notificationText = this._getLocalizedString("saveLogin", [brandShortName]);
let displayHost = this._getShortDisplayHost(aLogin.hostname);
let title = { text: displayHost, resource: aLogin.hostname };
let subtext = null;
if (aLogin.username) {
subtext = this._sanitizeUsername(aLogin.username);
}
// The callbacks in |buttons| have a closure to access the variables
// in scope here; set one to |this._pwmgr| so we can get back to pwmgr
// without a getService() call.
@ -190,22 +207,22 @@ LoginManagerPrompter.prototype = {
var buttons = [
{
label: this._getLocalizedString("saveButton"),
label: this._getLocalizedString("neverButton"),
callback: function() {
promptHistogram.add(PROMPT_NEVER);
pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
}
},
{
label: this._getLocalizedString("rememberButton"),
callback: function() {
pwmgr.addLogin(aLogin);
promptHistogram.add(PROMPT_ADD);
}
},
{
label: this._getLocalizedString("dontSaveButton"),
callback: function() {
promptHistogram.add(PROMPT_NOTNOW);
// Don't set a permanent exception
}
}
];
this._showLoginNotification("password-save", notificationText, buttons);
this._showLoginNotification("password-save", title, notificationText, buttons, subtext);
},
/*
@ -236,6 +253,9 @@ LoginManagerPrompter.prototype = {
notificationText = this._getLocalizedString("updatePasswordNoUser");
}
let displayHost = this._getShortDisplayHost(aOldLogin.hostname);
let title = { text: displayHost, resource: aOldLogin.hostname };
// The callbacks in |buttons| have a closure to access the variables
// in scope here; set one to |this._pwmgr| so we can get back to pwmgr
// without a getService() call.
@ -243,23 +263,23 @@ LoginManagerPrompter.prototype = {
let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION");
var buttons = [
{
label: this._getLocalizedString("updateButton"),
callback: function() {
self._updateLogin(aOldLogin, aNewPassword);
promptHistogram.add(PROMPT_UPDATE);
}
},
{
label: this._getLocalizedString("dontUpdateButton"),
callback: function() {
promptHistogram.add(PROMPT_NOTNOW);
// do nothing
}
},
{
label: this._getLocalizedString("updateButton"),
callback: function() {
self._updateLogin(aOldLogin, aNewPassword);
promptHistogram.add(PROMPT_UPDATE);
}
}
];
this._showLoginNotification("password-change", notificationText, buttons);
this._showLoginNotification("password-change", title, notificationText, buttons);
},
@ -377,10 +397,10 @@ LoginManagerPrompter.prototype = {
*/
_getLocalizedString : function (key, formatArgs) {
if (formatArgs)
return this._strBundle.formatStringFromName(
return this._strBundle.pwmgr.formatStringFromName(
key, formatArgs, formatArgs.length);
else
return this._strBundle.GetStringFromName(key);
return this._strBundle.pwmgr.GetStringFromName(key);
},

View File

@ -2,13 +2,10 @@
# 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/.
# 1st string is the username for the login, 2nd is the login's hostname.
# Note that long usernames may be truncated.
savePassword=Save password for "%1$S" on %2$S?
# String is the login's hostname
savePasswordNoUser=Save password on %S?
saveButton=Save
dontSaveButton=Don't save
# String will be replaced by brandShortName.
saveLogin=Would you like %S to remember this login?
rememberButton=Remember
neverButton=Never
# String is the login's hostname
updatePassword=Update saved password for %S?